1 Commits

Author SHA1 Message Date
bf04fa5077 coroutine 2025-09-14 13:53:12 +02:00
6 changed files with 213 additions and 210 deletions

View File

@@ -49,6 +49,5 @@ tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType<BootJar> { tasks.withType<BootJar> {
enabled = false enabled = false
} }

View File

@@ -3,13 +3,12 @@ package ltd.lulz.repository
import java.util.UUID import java.util.UUID
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
import org.springframework.data.r2dbc.repository.Query import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono
@Repository @Repository
interface AccountRepository : ReactiveCrudRepository<AccountEntity, UUID> { interface AccountRepository : CoroutineCrudRepository<AccountEntity, UUID> {
@Query("SELECT * FROM accounts WHERE id = :id FOR UPDATE NOWAIT") @Query("SELECT * FROM accounts WHERE id = :id FOR UPDATE NOWAIT")
fun findByIdForUpdate(id: UUID): Mono<AccountEntity> suspend fun findByIdForUpdate(id: UUID): AccountEntity?
} }

View File

@@ -8,7 +8,6 @@ import ltd.lulz.model.AccountEntity
import ltd.lulz.repository.AccountRepository import ltd.lulz.repository.AccountRepository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -17,28 +16,28 @@ class AccountService(
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
) { ) {
fun create( suspend fun create(
name: String, name: String,
amount: BigDecimal amount: BigDecimal
): Mono<AccountEntity> = accountRepository.save(AccountEntity(name = name, amount = amount)) ): AccountEntity = accountRepository.save(AccountEntity(name = name, amount = amount))
.doOnNext { log.debug { "account created with id: ${it.id}" } } .also { log.debug { "account created with id: ${it.id}" } }
fun getById( suspend fun getById(
id: UUID, id: UUID,
): Mono<AccountEntity> = accountRepository.findById(id) ): AccountEntity = accountRepository.findById(id)
.doOnNext { log.debug { "found account by id: ${it.id}" } } ?.also { log.debug { "found account by id: ${it.id}" } }
.switchIfEmpty(Mono.error(AccountNotFoundException())) ?: throw AccountNotFoundException()
@Transactional @Transactional
fun getForUpdateById( suspend fun getForUpdateById(
id: UUID, id: UUID,
): Mono<AccountEntity> = accountRepository.findByIdForUpdate(id) ): AccountEntity = accountRepository.findByIdForUpdate(id)
.doOnNext { log.trace { "account with id: ${it.id} locked for update" } } ?.also { log.trace { "account with id: ${it.id} locked for update" } }
.switchIfEmpty(Mono.error(AccountNotFoundException())) ?: throw AccountNotFoundException()
@Transactional @Transactional
fun save( suspend fun save(
entity: AccountEntity, entity: AccountEntity,
): Mono<AccountEntity> = accountRepository.save(entity) ): AccountEntity = accountRepository.save(entity)
.doOnNext { log.trace { "account with id: ${it.id} saved" } } .also { log.trace { "account with id: ${it.id} saved" } }
} }

View File

@@ -8,7 +8,6 @@ import ltd.lulz.exception.InsufficientFundsException
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -18,31 +17,36 @@ class TransactionService(
) { ) {
@Transactional @Transactional
fun deposit( suspend fun deposit(
id: UUID, id: UUID,
amount: BigDecimal, amount: BigDecimal,
): Mono<AccountEntity> = accountService.getForUpdateById(id) ): AccountEntity = accountService.getForUpdateById(id)
.map { it.copy(amount = it.amount + amount) } .let {
.doOnNext { log.trace { "Deposited $amount to account ${it.id}" } } log.trace { "Deposited $amount to account ${it.id}" }
.flatMap { accountService.save(it) } accountService.save(it.copy(amount = it.amount + amount))
}
@Transactional @Transactional
fun withdrawal( suspend fun withdrawal(
account: UUID, account: UUID,
amount: BigDecimal, amount: BigDecimal,
): Mono<AccountEntity> = accountService.getForUpdateById(account) ): AccountEntity = accountService.getForUpdateById(account)
.map { it.copy(amount = it.amount - amount) } .let {
.filter { it.amount >= ZERO } val entity = it.copy(amount = it.amount - amount)
.doOnNext { log.trace { "withdrawal $amount from account ${it.id}" } } if (entity.amount < ZERO) {
.switchIfEmpty(Mono.error(InsufficientFundsException())) throw InsufficientFundsException()
.flatMap { accountService.save(it) } }
log.trace { "withdrawal $amount from account ${it.id}" }
accountService.save(entity)
}
@Transactional @Transactional
fun transfer( suspend fun transfer(
account: UUID, account: UUID,
receiver: UUID, receiver: UUID,
amount: BigDecimal, amount: BigDecimal,
): Mono<Void> = withdrawal(account, amount) ) {
.zipWith(deposit(receiver, amount)) withdrawal(account, amount)
.then() deposit(receiver, amount)
}
} }

View File

@@ -1,21 +1,21 @@
package ltd.lulz.service package ltd.lulz.service
import io.mockk.every import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify
import java.math.BigDecimal import java.math.BigDecimal
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest
import ltd.lulz.exception.AccountNotFoundException import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
import ltd.lulz.repository.AccountRepository import ltd.lulz.repository.AccountRepository
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono import org.junit.jupiter.api.assertThrows
import reactor.test.StepVerifier
@Suppress("ReactiveStreamsUnusedPublisher", "MayBeConstant") @Suppress("MayBeConstant")
class AccountServiceTest { class AccountServiceTest {
companion object { companion object {
@@ -33,104 +33,102 @@ class AccountServiceTest {
} }
@Test @Test
fun `create account`() { fun `create account`() = runTest {
// given // given
val capture = slot<AccountEntity>() val capture = slot<AccountEntity>()
every { repository.save(capture(capture)) } answers { Mono.just(capture.captured.copy(id = uuid)) } coEvery { repository.save(capture(capture)) }
.answers { capture.captured.copy(id = uuid) }
// when stepped // when
StepVerifier.create(service.create(name, amount)) val result = service.create(name, amount)
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.save(any()) } // then
coVerify { repository.save(any()) }
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
} }
@Test @Test
fun `get by id`() { fun `get by id`() = runTest {
// given // given
val capture = slot<UUID>() val capture = slot<UUID>()
every { repository.findById(capture(capture)) } coEvery { repository.findById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) } .answers { AccountEntity(capture.captured, name, amount) }
// when stepped // when
StepVerifier.create(service.getById(uuid)) val result = service.getById(uuid)
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.findById(any(UUID::class)) } // then
coVerify { repository.findById(any(UUID::class)) }
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
} }
@Test @Test
fun `get by id - fail`() { fun `get by id - fail`() = runTest {
// given // given
every { repository.findById(any(UUID::class)) } returns Mono.empty() coEvery { repository.findById(any(UUID::class)) } returns null
// when stepped // when
StepVerifier.create(service.getById(uuid)) assertThrows<AccountNotFoundException> {
.expectError(AccountNotFoundException::class.java) service.getById(uuid)
.verify() }
verify { repository.findById(any(UUID::class)) } // then
coVerify { repository.findById(any(UUID::class)) }
} }
@Test @Test
fun `get for update by id`() { fun `get for update by id`() = runTest {
// given // given
val capture = slot<UUID>() val capture = slot<UUID>()
every { repository.findByIdForUpdate(capture(capture)) } coEvery { repository.findByIdForUpdate(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) } .answers { AccountEntity(capture.captured, name, amount) }
// when stepped // when
StepVerifier.create(service.getForUpdateById(uuid)) val result = service.getForUpdateById(uuid)
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.findByIdForUpdate(any(UUID::class)) } // then
coVerify { repository.findByIdForUpdate(any(UUID::class)) }
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
} }
@Test @Test
fun `get for update by id - fail`() { fun `get for update by id - fail`() = runTest {
// given // given
coEvery { repository.findByIdForUpdate(any(UUID::class)) } returns null
every { repository.findByIdForUpdate(any(UUID::class)) } returns Mono.empty() // when
assertThrows<AccountNotFoundException> {
service.getForUpdateById(uuid)
}
// when stepped // then
StepVerifier.create(service.getForUpdateById(uuid)) coVerify { repository.findByIdForUpdate(any(UUID::class)) }
.expectError(AccountNotFoundException::class.java)
.verify()
verify { repository.findByIdForUpdate(any(UUID::class)) }
} }
@Test @Test
fun `save change`() { fun `save change`() = runTest {
// given // given
val entity = AccountEntity(name = name, amount = amount) val entity = AccountEntity(name = name, amount = amount)
val capture = slot<AccountEntity>() val capture = slot<AccountEntity>()
every { repository.save(capture(capture)) } coEvery { repository.save(capture(capture)) }
.answers { Mono.just(capture.captured) } .answers { capture.captured }
// when stepped // when
StepVerifier.create(service.save(entity)) val result = service.save(entity)
.assertNext { result ->
assertThat(result).isEqualTo(entity)
}
.verifyComplete()
verify { repository.save(any()) } // then
coVerify { repository.save(any()) }
assertThat(result).isEqualTo(entity)
} }
} }

View File

@@ -1,11 +1,12 @@
package ltd.lulz.service package ltd.lulz.service
import io.mockk.every import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify
import java.math.BigDecimal import java.math.BigDecimal
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest
import ltd.lulz.exception.AccountNotFoundException import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.exception.InsufficientFundsException import ltd.lulz.exception.InsufficientFundsException
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
@@ -13,10 +14,9 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono import org.junit.jupiter.api.assertThrows
import reactor.test.StepVerifier
@Suppress("MayBeConstant", "ReactiveStreamsUnusedPublisher") @Suppress("MayBeConstant")
class TransactionServiceTest { class TransactionServiceTest {
companion object { companion object {
@@ -40,44 +40,44 @@ class TransactionServiceTest {
inner class Deposit { inner class Deposit {
@Test @Test
fun `deposit to account - success`() { fun `deposit to account - success`() = runTest {
// given // given
val deposit = BigDecimal.valueOf(1.10) val deposit = BigDecimal.valueOf(1.10)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.deposit(accountUuid, deposit)) val result = service.deposit(accountUuid, deposit)
.assertNext { result ->
assertThat(result.id).isEqualTo(accountUuid)
assertThat(result.name).isEqualTo(accountName)
assertThat(result.amount).isEqualTo(accountAmount + deposit)
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 1) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 1) { accountService.save(any()) }
assertThat(result.id).isEqualTo(accountUuid)
assertThat(result.name).isEqualTo(accountName)
assertThat(result.amount).isEqualTo(accountAmount + deposit)
} }
@Test @Test
fun `deposit to account - account not found`() { fun `deposit to account - account not found`() = runTest {
// given // given
val deposit = BigDecimal.valueOf(1.10) val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) coEvery { accountService.getForUpdateById(any()) } throws AccountNotFoundException()
// when stepped // when
StepVerifier.create(service.deposit(accountUuid, deposit)) assertThrows<AccountNotFoundException> {
.expectError(AccountNotFoundException::class.java) service.deposit(accountUuid, deposit)
.verify() }
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 0) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 0) { accountService.save(any()) }
} }
} }
@@ -85,62 +85,63 @@ class TransactionServiceTest {
inner class Withdrawal { inner class Withdrawal {
@Test @Test
fun `withdrawal from account - success`() { fun `withdrawal from account - success`() = runTest {
// given // given
val deposit = BigDecimal.valueOf(1.01) val deposit = BigDecimal.valueOf(1.01)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.withdrawal(accountUuid, deposit)) val result = service.withdrawal(accountUuid, deposit)
.assertNext { result ->
assertThat(result.id).isEqualTo(accountUuid)
assertThat(result.name).isEqualTo(accountName)
assertThat(result.amount).isEqualTo(accountAmount - deposit)
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 1) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 1) { accountService.save(any()) }
assertThat(result.id).isEqualTo(accountUuid)
assertThat(result.name).isEqualTo(accountName)
assertThat(result.amount).isEqualTo(accountAmount - deposit)
} }
@Test @Test
fun `withdrawal from account - insufficient founds`() { fun `withdrawal from account - insufficient founds`() = runTest {
// given // given
val deposit = BigDecimal.valueOf(1.10) val deposit = BigDecimal.valueOf(1.10)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
// when stepped // when
StepVerifier.create(service.withdrawal(accountUuid, deposit)) assertThrows<InsufficientFundsException> {
.expectError(InsufficientFundsException::class.java) service.withdrawal(accountUuid, deposit)
.verify() }
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 0) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 0) { accountService.save(any()) }
} }
@Test @Test
fun `withdrawal from account - account not found`() { fun `withdrawal from account - account not found`() = runTest {
// given // given
val deposit = BigDecimal.valueOf(1.10) val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) coEvery { accountService.getForUpdateById(any()) } throws AccountNotFoundException()
// when stepped // when
StepVerifier.create(service.withdrawal(accountUuid, deposit)) assertThrows<AccountNotFoundException> {
.expectError(AccountNotFoundException::class.java) service.withdrawal(accountUuid, deposit)
.verify() }
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 0) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 0) { accountService.save(any()) }
} }
} }
@@ -148,90 +149,93 @@ class TransactionServiceTest {
inner class Transfer { inner class Transfer {
@Test @Test
fun `transfer from account - success`() { fun `transfer from account - success`() = runTest {
// given // given
val transfer = BigDecimal.valueOf(1.00) val transfer = BigDecimal.valueOf(1.00)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
.andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } .andThenAnswer { AccountEntity(capture.captured, receiverName, receiverAmount) }
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) service.transfer(accountUuid, receiverUuid, transfer)
.verifyComplete()
verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 2) { accountService.save(any()) } coVerify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 2) { accountService.save(any()) }
} }
@Test @Test
fun `transfer from account - insufficient founds`() { fun `transfer from account - insufficient founds`() = runTest {
// given // given
val transfer = BigDecimal.valueOf(5.00) val transfer = BigDecimal.valueOf(5.00)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
.andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } .andThenAnswer { AccountEntity(capture.captured, receiverName, receiverAmount) }
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) assertThrows<InsufficientFundsException> {
.expectError(InsufficientFundsException::class.java) service.transfer(accountUuid, receiverUuid, transfer)
.verify() }
verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 0) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 0) { accountService.save(any()) }
} }
@Test @Test
fun `transfer from account - account not found`() { fun `transfer from account - account not found`() = runTest {
// given // given
val transfer = BigDecimal.valueOf(1.00) val transfer = BigDecimal.valueOf(1.00)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.error(AccountNotFoundException()) } .throws(AccountNotFoundException())
.andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } .andThenAnswer { AccountEntity(capture.captured, receiverName, receiverAmount) }
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) assertThrows<AccountNotFoundException> {
.expectError(AccountNotFoundException::class.java) service.transfer(accountUuid, receiverUuid, transfer)
.verify() }
verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 0) { accountService.save(any()) } coVerify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 0) { accountService.save(any()) }
} }
@Test @Test
fun `transfer from account - receiver not found`() { fun `transfer from account - receiver not found`() = runTest {
// given // given
val transfer = BigDecimal.valueOf(1.00) val transfer = BigDecimal.valueOf(1.00)
val capture = slot<UUID>() val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) } coEvery { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } .answers { AccountEntity(capture.captured, accountName, accountAmount) }
.andThenAnswer { Mono.error(AccountNotFoundException()) } .andThenThrows(AccountNotFoundException())
val entity = slot<AccountEntity>() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) } coEvery { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) } .answers { entity.captured }
// when stepped // when
StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) assertThrows<AccountNotFoundException> {
.expectError(AccountNotFoundException::class.java) service.transfer(accountUuid, receiverUuid, transfer)
.verify() }
verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } // then
verify(exactly = 1) { accountService.save(any()) } coVerify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) }
coVerify(exactly = 1) { accountService.save(any()) }
} }
} }
} }