kotlinunit-testingjunitmockk

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


I want to test following method:

@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)
  }

If there is an installment plan available the method storeInstallmentPlan(dtoIn.id, InstallmentPlanDTO.from(installmentPlan, installmentRecordIds)) will be called. The original method storeInstallmentPlan is already tested.

How can I mock storeInstallmentPlan inside my unit I want to test without repeat myself by testing storeInstallmentPlan again? I think I have to somehow use SpyK for doing this, but I don't know exactly how to do it. Maybe someone has a solution for me.

Thanks :)


Here my full test 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>>()) }
  }
}


Solution

  • Assuming that storeInstallmentPlan() is public, then you could annotate with @SpyK the class you are testing and then you would need to mock the storeInstallmentPlan() call in the updateDebtWithoutDocumentsByUserId ok - no installment plan available test.

    If you could add your complete test class, then we could provide a full example.