The day has come and now you want to test your data access layer. Or, perhaps, you need to test a service layer but to be sure that your tests are really close to the System in Production you don't want to mock persistence layer and/or database. And you use the Spring framework in the project. If so, this article will guide you through the simplest solution I'd found so far to create integration tests.
First of all, lets clear the terminology. Here by 'integration test' I mean a test which involves several software modules to produce the expected results. In very this article I'm going to speak about integration testing of database + (at least) one more module. Again, this "one more" module maybe be your DAO or Service-layer class which interacts with a DAO which, in turn, interacts with a database.
But let's dive in with a simple example. Disclaimer: all mentioned below source code is gathered into a single sample project accessible on the GitHub.
In our imaginary system we have a JPA entity class User
and simple persistence layer based on the Spring Data framework
UserRepository interface extends the Spring Data CrudRepository which means that Spring will implement for us generic CRUD operations. Type parametrization with User and Integer means that Repository works with JPA User entities that have an Integer identity. The method UserRepository.findByLastnameIgnoreCase() is our target to test. The method name had not been chosen accidentally - it is another portion of Spring Data black art:
The simplest approach to test UserRepository is to create, persist and retrieve a User in a classic main method. I intentionally omit all details regarding to the Spring/JPA/database configuration cause they are not important for the moment.
Here we get our Spring bean UserRepository, create and save dumb User and, in the end, search it with the method under test. As you see, we are logging an User object to check if the found User is really the one we search for. This approach - to dump an object details to visually control application correctness - is common in projects without tests. So now, when I see a domain object with overridden toString(), I'm suspecting no-tests-here smell. Okay, let's override User.toString()
INFO - [User{id=1, firstname='Bon', lastname='Jovi'}]
It works!.. if you run it.
But we want to be certain thus we are going to create a test and run it each time a project is built. Firstly, we'll choose good tools for that: maven for project building and test triggering, JUnit as a base test framework, Spring testing as an integration test framework and Spring core as a glue for all application components, H2 database as fast in-memory db, DbUnit to load initial test data right in database, Spring Test DbUnit library for nice annotation-based integration between Spring testing and DbUnit.
Ladies and gentlemen, let me introduce the BaseIntegrationTest - base class for all integration tests in the project.
What we have here:
First of all, UserRepositoryTest extends BaseIntegrationTest thus it inherits a behavior of parent class that was considered above. Next, the test class is marked with annotation @DatabaseSetup("userRepository.dbxml") which loads DbUnit xml file userRepository.dbxml into database before each (of two) test method execution. By default, CLEAN_INSERT mode is used for DbUnit data, which, as javadoc says, "deletes all rows from a database table when the tables is specified in the dataset and subsequently insert new contents". Let's take a look onto userRepository.dbxml.
As you guess, this file contains two User instances described in terms of database: an instance is persisted in the user table and has columns id, firstname and lastname with associated values.
Going further, take a look at how do we refer from the integration test to the initial database data to be loaded in. By default, Spring-DbUnit annotation @DatabaseSetup accepts class relative resources. Indeed, it's really handy to store initial test data file close to the test itself. But we use maven which dictates another convention - store test resource file in src/test/resources/same/package/as/used/for/test. Fortunately, it's easy to make maven load resources from the default directory and the directory containing a java test file. Here is a pom.xml snippet which does the trick:
In tests it's often very handy to compare expected and actual results equality with plain Java equals(). If your entities have overridden equals() (which compares valuable class fields but no the database id) then your test cases looks tiny but verbose. So we add equals() and hashCode() to the User class.
As you see, when all the preparations (maven, Spring, JPA, database, test framework) are done, it's really easy to create integration tests. If you want to know configuration details for the sample project we talked above or to run the UserRepositoryTest yourself than I urge you to clone my sample-projects GitHub repository, module it-spring-dbunit. However, you should be aware (and you'll see it yourself) that integration test execution takes plenty of time (compared to a regular unit test). To run an integration test: Spring context has to be launched, database connection to be set up, DbUnit data to be loaded, business (and persistence under the hood) logic to be executed. Thereby, huge amount of integration tests makes your project building procedure much longer. This is a payment for your confidence. Besides, there are many ways to speed up integration testing and, I hope, we'll speak about it someday.
First of all, lets clear the terminology. Here by 'integration test' I mean a test which involves several software modules to produce the expected results. In very this article I'm going to speak about integration testing of database + (at least) one more module. Again, this "one more" module maybe be your DAO or Service-layer class which interacts with a DAO which, in turn, interacts with a database.
But let's dive in with a simple example. Disclaimer: all mentioned below source code is gathered into a single sample project accessible on the GitHub.
In our imaginary system we have a JPA entity class User
and simple persistence layer based on the Spring Data framework
UserRepository interface extends the Spring Data CrudRepository which means that Spring will implement for us generic CRUD operations. Type parametrization with User and Integer means that Repository works with JPA User entities that have an Integer identity. The method UserRepository.findByLastnameIgnoreCase() is our target to test. The method name had not been chosen accidentally - it is another portion of Spring Data black art:
- findBy prefix tells the Spring Data that here is a search-and-retrieve method. What to search we'll know a bit later but what to retrieve is pretty clear stated in method declaration - a List of Users.
- Lastname is a User class field name - the main search criteria for this method.
- IgnoreCase - just a Spring Data spell which modifies the main search criteria in a desired way.
The simplest approach to test UserRepository is to create, persist and retrieve a User in a classic main method. I intentionally omit all details regarding to the Spring/JPA/database configuration cause they are not important for the moment.
Here we get our Spring bean UserRepository, create and save dumb User and, in the end, search it with the method under test. As you see, we are logging an User object to check if the found User is really the one we search for. This approach - to dump an object details to visually control application correctness - is common in projects without tests. So now, when I see a domain object with overridden toString(), I'm suspecting no-tests-here smell. Okay, let's override User.toString()
@Override public String toString() { return "User{" + "id=" + id + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + '}'; }Now we run Main.main() and see in log:
INFO - [User{id=1, firstname='Bon', lastname='Jovi'}]
It works!.. if you run it.
But we want to be certain thus we are going to create a test and run it each time a project is built. Firstly, we'll choose good tools for that: maven for project building and test triggering, JUnit as a base test framework, Spring testing as an integration test framework and Spring core as a glue for all application components, H2 database as fast in-memory db, DbUnit to load initial test data right in database, Spring Test DbUnit library for nice annotation-based integration between Spring testing and DbUnit.
Ladies and gentlemen, let me introduce the BaseIntegrationTest - base class for all integration tests in the project.
What we have here:
- With JUnit annotation @RunWith we tell JUnit that a test involves Spring (see javadoc for the SpringJUnit4ClassRunner)
- @ContextConfiguration("classpath:applicationConfiguration.xml") tells Spring what context config to choose.
- @Transactional is a marker annotation for the Spring which runs a transactional test in separate transaction and rollback it after the test is accomplished.
- With help of @TestExecutionListeners we get inside the Spring testing framework to add annotated DbUnit support. By default, Spring comes with three TestExecutionListeners: DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener and TransactionalTestExecutionListener. In BaseIntegrationTest we substitute one of standard listeners, TransactionalTestExecutionListener, with the TransactionDbUnitTestExecutionListener which integrates Spring tests (marked with @DatabaseSetup) with DbUnit. How it is used you'll see in UserRepositoryTest.
First of all, UserRepositoryTest extends BaseIntegrationTest thus it inherits a behavior of parent class that was considered above. Next, the test class is marked with annotation @DatabaseSetup("userRepository.dbxml") which loads DbUnit xml file userRepository.dbxml into database before each (of two) test method execution. By default, CLEAN_INSERT mode is used for DbUnit data, which, as javadoc says, "deletes all rows from a database table when the tables is specified in the dataset and subsequently insert new contents". Let's take a look onto userRepository.dbxml.
As you guess, this file contains two User instances described in terms of database: an instance is persisted in the user table and has columns id, firstname and lastname with associated values.
Going further, take a look at how do we refer from the integration test to the initial database data to be loaded in. By default, Spring-DbUnit annotation @DatabaseSetup accepts class relative resources. Indeed, it's really handy to store initial test data file close to the test itself. But we use maven which dictates another convention - store test resource file in src/test/resources/same/package/as/used/for/test. Fortunately, it's easy to make maven load resources from the default directory and the directory containing a java test file. Here is a pom.xml snippet which does the trick:
In tests it's often very handy to compare expected and actual results equality with plain Java equals(). If your entities have overridden equals() (which compares valuable class fields but no the database id) then your test cases looks tiny but verbose. So we add equals() and hashCode() to the User class.
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (firstname != null ? !firstname.equals(user.firstname) : user.firstname != null) return false; if (lastname != null ? !lastname.equals(user.lastname) : user.lastname != null) return false; return true; } @Override public int hashCode() { int result = firstname != null ? firstname.hashCode() : 0; result = 31 * result + (lastname != null ? lastname.hashCode() : 0); return result; }Now, when we know everything, it's easy to understand what two test are checking. The first one, findByLastnameIgnoreCase_noSuchUser(), checks that in a database there is no User with lastname equal (ignore case) to "Lastname". The last one, findByLastnameIgnoreCase_ok(), checks that in a database there is only one User instance with lastname equal (ignore case) to "lastname1".
As you see, when all the preparations (maven, Spring, JPA, database, test framework) are done, it's really easy to create integration tests. If you want to know configuration details for the sample project we talked above or to run the UserRepositoryTest yourself than I urge you to clone my sample-projects GitHub repository, module it-spring-dbunit. However, you should be aware (and you'll see it yourself) that integration test execution takes plenty of time (compared to a regular unit test). To run an integration test: Spring context has to be launched, database connection to be set up, DbUnit data to be loaded, business (and persistence under the hood) logic to be executed. Thereby, huge amount of integration tests makes your project building procedure much longer. This is a payment for your confidence. Besides, there are many ways to speed up integration testing and, I hope, we'll speak about it someday.
this article and the accompanying Github project was very helpful for me as i added dbunit test cases to my project. thanks for posting!
ReplyDelete