Blogs

Testing Spring Data JPA Using ScalaTest

Category
Software development
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

Spring Data JPA and ScalaTest Notch
― by Goran Pugar
Using Spring Data JPA and Scala… Seriously?

Previous post

Check our blog list.

Next

Blog

Notch Named Top Custom Software Developer in Croatia by Clutch

Top Developers Notch Clutch 2021

Company

Our people really love it here

Evolution of expertise

The agency was founded in 2014 by seasoned industry veterans with experience from large enterprises. A group of Java engineers then evolved into a full-service company that builds complex web and mobile applications. 

Flexibility is our strong suit — both large enterprises and startups use our services. We are now widely recognized as the leading regional experts in the Java platform and Agile approach.

We make big things happen and we are proud of that.

Personal development

If you join our ranks you’ll be able hone your coding skills on relevant projects for international clients.

Our diverse client portfolio enables our engineers to work on complex long term projects like core technologies for Delivery Hero, Blockchain and NFTs for Fantasy Football or cockpit interface for giants like Strabag. 

Constant education and knowledge sharing are part of our company culture. Highly experienced mentors will help you out and provide real-time feedback and support.

Contact

We’d love to hear from you