Testing Spring Data JPA Using ScalaTest

In the previous blog post, we have introduced Spring Data JPA into our Scala domain, this post will take us a step further in building support for running integration tests using the ScalaTest library.
ScalaTest is a very flexible and quite popular testing tool in the Scala ecosystem. It has a rich and extensible feature set and it is easy to use.
ScalaTest integrates well with most used eco-systems such as JUnit, TestNG, sbt, Maven, Gradle, ScalaCheck, JMock, EasyMock, Mockito, ScalaMock, and so on.
However, there is one caveat to take into account. This demo uses ScalaTest in combination with JUnit4 library, since at the moment of writing this post there is still no support for JUnit5. To see more details on using it with JUnit5 please take a look at the issue.
The entire project with complete code covered in the previous blog post together with ScalaTest implementation is available at Github example project scala-spring-jpa.
Groundwork
In order for our TestScala suite to work with SpringRunner and @DataJpaTest annotation we need to do some of the groundwork.
For this, we created the abstract class BaseScalaJpaTest which implements all the ScalaTest traits we want to use e.g. AnyFunSpecLike, Matchers, or BeforeAndAfterAll. This gives us all the tooling to really use TestScala and its treats:
abstract class BaseScalaJpaTest
extends AnyFunSpecLike
with Matchers
with BeforeAndAfterAll
with BeforeAndAfterEach
Next, we want to define a test runner (SpringRunner) and spring auto-configuration for the Spring Data JPA tests. We add following annotations to our BaseScalaJpaTest:
@RunWith(classOf[SpringRunner])
@DataJpaTest
Note that we are not adding the @Transactional anywhere.
In order for our tests to work with Spring we need to define TestContextManager and initialize/destroy it in beforeAll/afterAll methods.
As noted in JavaDocs, TestContextManager is the main entry point into the Spring TestContext Framework. Specifically, a TestContextManager is responsible for managing a single TestContext and signaling events to each registered TestExecutionListener at the following test execution points.
val manager = new TestContextManager(this.getClass)
override protected def beforeAll(): Unit =
manager.prepareTestInstance(this)
override protected def afterAll(): Unit =
manager.afterTestClass()
In this base class, we also want to initialize our test data. Ideally, we would use some of the convenient tools or libraries for that like DbUnit, but in our simple case, we do it programmatically by just using the EntityManager to persist our model objects in the in-memory database.
So as mentioned above, we need EntityManager. This requires the injection of a transaction manager and transaction template. Here is an example code on how we can create persist method for creating entities:
@Autowired
override var transactionManager: PlatformTransactionManager = _
@Autowired
var transactionTemplate: TransactionTemplate = _
@Autowired
var entityManager: TestEntityManager = _
def persist[E](e: E): E =
transactionTemplate.execute(_ => entityManager.persist(e))
First simple ScalaTest for Spring JPA Data unit test
By defining all of the above things in our base class we are ready to write our first unit test.
We create a test class for MeterRepositoryImpl which extends our base class. As we test MeterRepository we, of course, use autowiring to inject it into our test instance.
Let’s use Fun Spec to describe our test. First test we write is unit test for add method:
class MeterRepositoryImplSpec extends BaseScalaJpaTest {
@Autowired
var meterRepository: MeterRepository = _
describe("add") {
it("should save meter") {
val meterName = "meter-to-save"
val meterLabel = "labelForSavedMeter"
val meter = Meter(meterName, Option(meterLabel))
val savedMeter = meterRepository.add(meter)
savedMeter.id should not be null
savedMeter.name should equal(meterName)
savedMeter.label should equal(meterLabel)
}
}
}
First test finished, it’s green, but not everything that goes green, is working as it’s supposed to.
Managing transaction Rollback for unit tests
The above test creates a Meter object and stores it in the database. If we create more unit tests inside of MeterRepositoryImpl we will see that the object created in the add method test will remain in our repository. This will make our tests inconsistent and faulty if they’re being run in an isolated fashion.
Because of that we want to have a transaction rollback after each unit test run. In order to achieve it we will introduce RollbackEachTransactionalTest trait which will extend the stackable BeforeAndAfterEachTestData trait.
BeforeAndAfterEachTestData is used when we want to stack traits that perform side-effects before or after tests, rather than at the beginning or end of tests, when you need access to any test data in the before and/or after code.
What we actually do there is that, in the beforeEach method, we use SpringTransactionAnnotationParser to get test class transactional annotation from which we create a transaction and store it. After the test finishes we just make sure that our transaction gets rollback by running explicit rollback on it inside of afterEach method.
trait RollbackEachTransactionalTest extends BeforeAndAfterEachTestData { this: Suite =>
var transactionManager: PlatformTransactionManager
var rollbackContext: TransactionStatus = _
override protected def beforeEach(testData: TestData): Unit = {
super.beforeEach(testData)
val annotationParser = new SpringTransactionAnnotationParser
val transactionDefinition = new DefaultTransactionDefinition()
transactionDefinition.setName(testData.name)
Option(annotationParser.parseTransactionAnnotation(this.getClass)).foreach(
annotationAttribute => {
transactionDefinition.setIsolationLevel(annotationAttribute.getIsolationLevel)
transactionDefinition.setPropagationBehavior(annotationAttribute.getPropagationBehavior)
transactionDefinition.setReadOnly(annotationAttribute.isReadOnly)
}
)
rollbackContext = transactionManager.getTransaction(transactionDefinition)
}
override protected def afterEach(testData: TestData): Unit = {
super.afterEach(testData)
transactionManager.rollback(rollbackContext)
}
}
Wrap-up…
In the end all we need to do is, just extend RollbackEachTransactionalTest trait in BaseScalaJpaTest class and all of our unit tests will execute as expected and clean up the repository after running.
class MeterRepositoryImplSpec extends BaseScalaJpaTest with RollbackEachTransactionalTest
We have built a simple Spring JPA Repository example using scala and tested it with the usage of ScalaTest library.
You may ask why not simply use JUnit Jupiter and Extensions or something that works with SpringRunner. Well, it’s a matter of style and taste in my opinion.
Blog