0

I'm trying to get a Kotlin function to operate transactionally in Spring Boot, and I've looked at several sources for information, such as https://codete.com/blog/5-common-spring-transactional-pitfalls/ and Spring @Transaction method call by the method within the same class, does not work?. I believe I have the prerequisites necessary for the @Transactional annotation to work - the function is public and being invoked externally, if my understanding is correct. My code currently looks like this:

interface CreateExerciseInstance {
    operator fun invoke(input: CreateExerciseInstanceInput): OpOutcome<CreateExerciseInstanceOutput>
}

@Component
class CreateExerciseInstanceImpl constructor(
    private val exerciseInstanceRepository: ExerciseInstanceRepository, // @Repository
    private val activityInstanceRepository: ActivityInstanceRepository, // @Repository
    private val exerciseInstanceStepRepository: ExerciseInstanceStepRepository // @Repository
) : CreateExerciseInstance {

    @Suppress("TooGenericExceptionCaught")
    @Transactional
    override fun invoke(input: CreateExerciseInstanceInput): OpOutcome<CreateExerciseInstanceOutput> {
        ...
        val exerciseInstanceRecord = ... // no in-place modification of repository data
        val activityInstanceRecords = ...
        val exerciseInstanceStepRecords = ...

        return try {
            exerciseInstanceRepository.save(exerciseInstanceRecord)
            activityInstanceRepository.saveAll(activityInstanceRecords)
            exerciseInstanceStepRepository.saveAll(exerciseInstanceStepRecords)
            Outcome.Success(...)
        } catch (e: Exception) {
            Outcome.Failure(...)
        }
    }

}

My test currently looks like this:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@Transactional
class CreateExerciseInstanceTest {
    @Autowired
    private lateinit var exerciseInstanceRepository: ExerciseInstanceRepository
    @Autowired
    private lateinit var exerciseInstanceStepRepository: ExerciseInstanceStepRepository
    @Autowired
    private lateinit var activityInstanceRepository: ActivityInstanceRepository

    @Test
    fun `does not commit to exercise instance or activity repositories when exercise instance step repository throws exception`() {
        ... // data setup

        val exerciseInstanceStepRepository = mockk<ExerciseInstanceStepRepository>()
        val exception = Exception("Something went wrong")
        every { exerciseInstanceStepRepository.save(any<ExerciseInstanceStepRecord>()) } throws exception

        val createExerciseInstance = CreateExerciseInstanceImpl(
            exerciseInstanceRepository = exerciseInstanceRepository,
            activityInstanceRepository = activityInstanceRepository,
            exerciseInstanceStepRepository = exerciseInstanceStepRepository
        )
        val outcome = createExerciseInstance(...)
        assert(outcome is Outcome.Failure)

        val exerciseInstances = exerciseInstanceRepository.findAll()
        val activityInstances = activityInstanceRepository.findAll()
        assertThat(exerciseInstances.count()).isEqualTo(0)
        assertThat(activityInstances.count()).isEqualTo(0)
    }
}

The test fails with:

org.opentest4j.AssertionFailedError: 
Expecting:
 <1>
to be equal to:
 <0>
but was not.

at assertThat(exerciseInstances.count()).isEqualTo(0). Is the function actually non-public or being invoked internally? Have I missed some other prerequisite?

Ashok Bhaskar
  • 361
  • 2
  • 16

1 Answers1

0

This test doesn't say anything about your component not being transactional.

First, you create an instance yourself rather than using the one created by Spring. So Spring knows nothing about this instance, and can't possibly warp it into a transactional proxy.

Second, the component doesn't throw any runtime exception, So Spring doesn't rollback the transaction.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Thanks - how could I modify the test to rectify these issues? – Ashok Bhaskar Dec 16 '19 at 21:31
  • If you want to prove that it is transactional, there is not much you can do, since the code can't possibly throw an exception causing a rollback. – JB Nizet Dec 16 '19 at 22:13
  • If it's not a runtime exception, what kind of exception is being thrown by the `mockk` + `Exception()` combination? – Ashok Bhaskar Dec 17 '19 at 18:28
  • Well, it's not a runtie exception since it's an instance of Exception, and not not instance of RuntimeException. But even if it was, it's thrown by your mock repository, but **caught** by your service method. So the service method doesn't throw any exception. – JB Nizet Dec 17 '19 at 18:50