1 Commits

Author SHA1 Message Date
8743419447 reactive 2025-09-14 13:57:25 +02:00
6 changed files with 210 additions and 213 deletions

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ 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 {}
@@ -16,28 +17,28 @@ class AccountService(
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
) { ) {
suspend fun create( fun create(
name: String, name: String,
amount: BigDecimal amount: BigDecimal
): AccountEntity = accountRepository.save(AccountEntity(name = name, amount = amount)) ): Mono<AccountEntity> = accountRepository.save(AccountEntity(name = name, amount = amount))
.also { log.debug { "account created with id: ${it.id}" } } .doOnNext { log.debug { "account created with id: ${it.id}" } }
suspend fun getById( fun getById(
id: UUID, id: UUID,
): AccountEntity = accountRepository.findById(id) ): Mono<AccountEntity> = accountRepository.findById(id)
?.also { log.debug { "found account by id: ${it.id}" } } .doOnNext { log.debug { "found account by id: ${it.id}" } }
?: throw AccountNotFoundException() .switchIfEmpty(Mono.error(AccountNotFoundException()))
@Transactional @Transactional
suspend fun getForUpdateById( fun getForUpdateById(
id: UUID, id: UUID,
): AccountEntity = accountRepository.findByIdForUpdate(id) ): Mono<AccountEntity> = accountRepository.findByIdForUpdate(id)
?.also { log.trace { "account with id: ${it.id} locked for update" } } .doOnNext { log.trace { "account with id: ${it.id} locked for update" } }
?: throw AccountNotFoundException() .switchIfEmpty(Mono.error(AccountNotFoundException()))
@Transactional @Transactional
suspend fun save( fun save(
entity: AccountEntity, entity: AccountEntity,
): AccountEntity = accountRepository.save(entity) ): Mono<AccountEntity> = accountRepository.save(entity)
.also { log.trace { "account with id: ${it.id} saved" } } .doOnNext { log.trace { "account with id: ${it.id} saved" } }
} }

View File

@@ -8,6 +8,7 @@ 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 {}
@@ -17,36 +18,31 @@ class TransactionService(
) { ) {
@Transactional @Transactional
suspend fun deposit( fun deposit(
id: UUID, id: UUID,
amount: BigDecimal, amount: BigDecimal,
): AccountEntity = accountService.getForUpdateById(id) ): Mono<AccountEntity> = accountService.getForUpdateById(id)
.let { .map { it.copy(amount = it.amount + amount) }
log.trace { "Deposited $amount to account ${it.id}" } .doOnNext { log.trace { "Deposited $amount to account ${it.id}" } }
accountService.save(it.copy(amount = it.amount + amount)) .flatMap { accountService.save(it) }
}
@Transactional @Transactional
suspend fun withdrawal( fun withdrawal(
account: UUID, account: UUID,
amount: BigDecimal, amount: BigDecimal,
): AccountEntity = accountService.getForUpdateById(account) ): Mono<AccountEntity> = accountService.getForUpdateById(account)
.let { .map { it.copy(amount = it.amount - amount) }
val entity = it.copy(amount = it.amount - amount) .filter { it.amount >= ZERO }
if (entity.amount < ZERO) { .doOnNext { log.trace { "withdrawal $amount from account ${it.id}" } }
throw InsufficientFundsException() .switchIfEmpty(Mono.error(InsufficientFundsException()))
} .flatMap { accountService.save(it) }
log.trace { "withdrawal $amount from account ${it.id}" }
accountService.save(entity)
}
@Transactional @Transactional
suspend fun transfer( fun transfer(
account: UUID, account: UUID,
receiver: UUID, receiver: UUID,
amount: BigDecimal, amount: BigDecimal,
) { ): Mono<Void> = withdrawal(account, amount)
withdrawal(account, amount) .zipWith(deposit(receiver, amount))
deposit(receiver, amount) .then()
}
} }

View File

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

View File

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