繁体   English   中英

MockK:如何模拟在另一个被测方法中调用的已测试方法(Kotlin)

[英]MockK: How to mock an already tested method called inside another method under test (Kotlin)

我想测试以下方法:

@Transactional
  override suspend fun updateDebtWithoutDocumentsByUserId(userId: Long, dtoIn: DebtDTO): DebtDTO {
    if (dtoIn.id == null) {
      throw NotFoundException("Can only update existing debt")
    }
    val d = debtRepository.findById(dtoIn.id) ?: throw NotFoundException("Could not find debt ${dtoIn.id}")
    val updated = debtRepository.save(
      d.copy(
        title = dtoIn.title,
        amount = dtoIn.amount,
        category = dtoIn.category,
      )
    )
    if (dtoIn.amount != d.amount) {
      // update potentially existing installment plan.
      // Note that this guarantees that the installment amount is kept in bounds and all installment records are updated
      // accordingly.
      val installmentPlan = installmentPlanRepository.findByDebtId(dtoIn.id)
      if (installmentPlan != null) {
        val installmentRecordIds: List<Long> = installmentRepository.findByDebtId(dtoIn.id)
          .toList()
          .map { it.recordId }
        storeInstallmentPlan(dtoIn.id, InstallmentPlanDTO.from(installmentPlan, installmentRecordIds))
          .collect() // ensure that the flow is executed.
      }
    }
    val documents = debtDocumentRepository.findByDebtId(dtoIn.id)
      .toSet()
      .map { DocumentDTO.from(it) }
      .associateBy { it.id!! }
    return DebtDTO.from(updated, documents)
  }

如果有可用的分期付款计划,将调用方法storeInstallmentPlan(dtoIn.id, InstallmentPlanDTO.from(installmentPlan, installmentRecordIds)) 原始方法storeInstallmentPlan已经过测试。

如何通过再次测试storeInstallmentPlan在我想测试的单元内模拟storeInstallmentPlan而不会重复自己? 我想我必须以某种方式使用SpyK来做到这一点,但我不知道该怎么做。 也许有人对我有解决方案。

谢谢:)


这是我的完整测试 class

@ExtendWith(MockKExtension::class)
internal class PlanServiceFeatureImplTest {

  @MockK
  lateinit var debtRepository: DebtRepository

  @MockK
  lateinit var debtDocumentRepository: DebtDocumentRepository

  @MockK
  lateinit var installmentPlanRepository: InstallmentPlanRepository

  @MockK
  lateinit var installmentRepository: InstallmentRepository

  @MockK
  lateinit var recordService: RecordService

  @MockK
  lateinit var userService: UserService

  @InjectMockKs
  lateinit var planService: PlanServiceFeatureImpl

  @Test
  fun `updateDebtWithoutDocumentsByUserId ok - no installment plan available`() {
    val userId = 123L
    val dtoIn = DebtDTO(
      id = 1L,
      title = "debt1New",
      amount = BigDecimal.valueOf(2000),
      category = DebtCategory.dangerous,
      documents = mapOf(),
    )
    val oldDebt = Debt(
      id = 1L,
      userId = userId,
      title = "debt1",
      amount = BigDecimal.valueOf(1000),
      category = DebtCategory.credit,
    )
    assertThat(dtoIn.title).isNotEqualTo(oldDebt.title)
    assertThat(dtoIn.amount).isNotEqualTo(oldDebt.amount)
    assertThat(dtoIn.category).isNotEqualTo(oldDebt.category)

    coEvery { debtRepository.findById(dtoIn.id!!) } returns oldDebt
    val doc1 = DebtDocument(
      id = 11L,
      debtId = dtoIn.id!!,
      title = "doc1",
      bytes = byteArrayOf(0x01),
    )
    coEvery { debtRepository.save(any()) } answers { arg(0) }
    coEvery { debtDocumentRepository.findByDebtId(dtoIn.id!!) } returns flowOf(doc1)

    //no installment plan available
    coEvery { installmentPlanRepository.findByDebtId(dtoIn.id!!) } returns null

    runBlocking {
      val actual = planService.updateDebtWithoutDocumentsByUserId(userId, dtoIn)
      assertThat(actual).isEqualTo(
        dtoIn.copy(
          documents = mapOf(doc1.id!! to DocumentDTO.from(doc1)),
        )
      )
    }
    coVerify {
      debtRepository.save(withArg {
        assertThat(it.id).isEqualTo(dtoIn.id)
        assertThat(it.userId).isEqualTo(userId)
        assertThat(it.title).isEqualTo(dtoIn.title)
        assertThat(it.amount).isEqualTo(dtoIn.amount)
        assertThat(it.category).isEqualTo(dtoIn.category)
      })
    }
  }

  @Test
  fun `updateDebtWithoutDocumentsByUserId ok - installment plan available`() {
    val userId = 123L
    val dtoIn = DebtDTO(
      id = 1L,
      title = "debt1New",
      amount = BigDecimal.valueOf(2000),
      category = DebtCategory.dangerous,
      documents = mapOf(),
    )
    val oldDebt = Debt(
      id = 1L,
      userId = userId,
      title = "debt1",
      amount = BigDecimal.valueOf(1000),
      category = DebtCategory.credit,
    )
    assertThat(dtoIn.title).isNotEqualTo(oldDebt.title)
    assertThat(dtoIn.amount).isNotEqualTo(oldDebt.amount)
    assertThat(dtoIn.category).isNotEqualTo(oldDebt.category)

    coEvery { debtRepository.findById(dtoIn.id!!) } returns oldDebt
    val doc1 = DebtDocument(
      id = 11L,
      debtId = dtoIn.id!!,
      title = "doc1",
      bytes = byteArrayOf(0x01),
    )
    coEvery { debtRepository.save(any()) } answers { arg(0) }
    coEvery { debtDocumentRepository.findByDebtId(dtoIn.id!!) } returns flowOf(doc1)

    //installment plan available
    val plan = mockk<InstallmentPlan>()
    coEvery { installmentPlanRepository.findByDebtId(dtoIn.id!!) } returns plan
    coEvery { installmentRepository.findByDebtId(dtoIn.id!!) } returns flowOf()
    /*

                Here I need to test storeInstallmentPlan / mock of it

     */

    runBlocking {
      val actual = planService.updateDebtWithoutDocumentsByUserId(userId, dtoIn)
      assertThat(actual).isEqualTo(
        dtoIn.copy(
          documents = mapOf(doc1.id!! to DocumentDTO.from(doc1)),
        )
      )
    }
    coVerify {
      debtRepository.save(withArg {
        assertThat(it.id).isEqualTo(dtoIn.id)
        assertThat(it.userId).isEqualTo(userId)
        assertThat(it.title).isEqualTo(dtoIn.title)
        assertThat(it.amount).isEqualTo(dtoIn.amount)
        assertThat(it.category).isEqualTo(dtoIn.category)
      })
    }
  }

  @Test
  fun `storeInstallmentPlan ok`(@MockK user: User) {
    val debtId = 123L
    val debtAmount = BigDecimal.valueOf(1000)
    val installmentAmount = BigDecimal.valueOf(500)
    val interval = Repeat.monthly

    val firstPaymentDate = LocalDate.of(2021, 12, 27)
      .atStartOfDay(Time.DEFAULT_TIME_ZONE).toOffsetDateTime()

    val planDTO = InstallmentPlanDTO(
      interval = interval,
      firstPaymentDate = firstPaymentDate,
      amount = installmentAmount,
      installments = listOf()
    )
    val debt = Debt(
      userId = 32L,
      title = "debt1",
      amount = debtAmount,
      category = DebtCategory.credit
    )
    val plan = InstallmentPlan(
      id = 122L,
      debtId = debtId,
      interval = interval,
      firstPaymentDate = firstPaymentDate,
      amount = installmentAmount
    )

    val installment1 = Installment(
      id = 12L,
      debtId = debtId,
      recordId = 15L
    )
    val installment2 = Installment(
      id = 13L,
      debtId = debtId,
      recordId = 16L
    )

    val installments = listOf(
      WalletRecord(
        userId = debt.userId,
        type = RecordType.debt_rate,
        amount = installmentAmount,
        title = debt.title,
        category = RecordCategory.debt_rate,
        repeat = plan.interval,
        startDate = ZonedDateTime.parse("2021-11-28T00:00+01:00[Europe/Berlin]").toOffsetDateTime(),
        endDate = ZonedDateTime.parse("2021-12-27T00:00+01:00[Europe/Berlin]").toOffsetDateTime()
      ),
      WalletRecord(
        userId = debt.userId,
        type = RecordType.debt_rate,
        amount = installmentAmount,
        title = debt.title,
        category = RecordCategory.debt_rate,
        repeat = plan.interval,
        startDate = ZonedDateTime.parse("2021-12-28T00:00+01:00[Europe/Berlin]").toOffsetDateTime(),
        endDate = ZonedDateTime.parse("2022-01-27T00:00+01:00[Europe/Berlin]").toOffsetDateTime()
      )
    )

    every { user.tz } returns "Europe/Berlin"
    coEvery { debtRepository.findById(debtId) } returns debt

    //installment plan for debt with debtId exists
    coEvery { installmentPlanRepository.findByDebtId(debtId) } returns plan

    //update installment plan
    coEvery {
      installmentPlanRepository.save(planDTO.copy(amount = installmentAmount).toEntity(id = plan.id, debtId = debtId))
    } returns InstallmentPlan(plan.id, debtId, interval, firstPaymentDate, installmentAmount)

    //find and delete all previous installments/records for debt with debtId
    coEvery { installmentRepository.findByDebtId(debtId) } returns flowOf(installment1, installment2)
    coEvery { installmentRepository.deleteAllById(listOf(installment1.id!!, installment2.id!!)) } just Runs
    coEvery { recordService.deleteAll(listOf(installment1.recordId, installment2.recordId)) } just Runs

    //replace all new installments/records
    coEvery { userService.findById(debt.userId) } returns user
    coEvery { recordService.saveAll(installments) } returns flowOf()
    coEvery { installmentRepository.saveAll(any<Flow<Installment>>()) } returns flowOf()

    runBlocking { planService.storeInstallmentPlan(debtId, planDTO) }

    coVerify { installmentPlanRepository.save(
      planDTO.copy(amount = installmentAmount).toEntity(id = plan.id, debtId = debtId)
    ) }
    coVerify { recordService.saveAll(installments) }
    coVerify { installmentRepository.saveAll(any<Flow<Installment>>()) }
  }
}

假设storeInstallmentPlan()是公开的,那么您可以使用@SpyK注释您正在测试的 class,然后您需要在 updateDebtWithoutDocumentsByUserId 中模拟storeInstallmentPlan()调用updateDebtWithoutDocumentsByUserId ok - no installment plan available测试。

如果您可以添加完整的测试 class,那么我们可以提供一个完整的示例。

如何通过再次测试 storeInstallmentPlan 在我想测试的单元内模拟 storeInstallmentPlan 而不会重复自己?

您可以监视被测实例。 伪代码:

@Test
fun myTest() {
    val objectUnderTest = createTestObject()
    val spy = spyk(objectUnderTest)

    // Set up mocks used internally, etc

    spy.updateDebtWithoutDocumentsByUserId() // Call method on spy

    verify { spy.storeInstallmentPlan(...) }
}

如果storeInstallmentPlan尚未公开,您可以使用@VisibileForTesting对其进行标记。

或者,考虑在计划存在时测试调用方法的结果 当计划存在时,您可以观察到调用该方法的预期结果有哪些副作用?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM