📜 ⬆️ ⬇️

Integration of DBUnit and Spring TestContext Framework

With the emergence of the TestContext framework in Spring 2.5, integration testing of code that works with a database has become much easier. There are annotations for declarative indication of the context in which the test should be performed, annotations for transaction management within the test, as well as the basic test classes for JUnit and TestNG . In this article, I will describe the integration option of the TestContext framework with DBUnit , which allows you to initialize the database and verify its state with the expected one when the test is completed.

Consider a simple example: we need to test the correct storage of a domain object in the database.
@Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; ... 


DAO, responsible for maintaining the object:

 public class JpaPersonDao implements PersonDao { @PersistenceContext private EntityManager em; public void save(Person person) { em.persist(person); } } 

')
It is worth noting that the integration testing of DAO implies comprehensive testing of DAO, mapping of the domain object and persistent provider. In our case, we use Hibernate as the last. For testing, we will create a Spring context testContext.xml with the following content:

  <!--       @Transactional --> <tx:annotation-driven/> <!--     HSQLDB --> <jdbc:embedded-database id="dataSource" /> <!-- - --> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence"/> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="ru.kacit.commons.test.dbunit"/> <!-- ,      --> <property name="jpaPropertyMap"> <map> <entry key="hibernate.show_sql" value="true"/> <entry key="hibernate.format_sql" value="true"/> <entry key="hibernate.hbm2ddl.auto" value="create"/> </map> </property> </bean> <!--    --> <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <!--  DAO --> <bean class="ru.kacit.commons.test.dbunit.JpaPersonDao" /> 


Now we will create a test class by extending the standard Spring TestContext Framework class for transactional tests based on JUnit. The @ContextConfiguration annotation points to the context (located, in our case, in the classpath) in which this test should be performed. This allows us to inject the tested DAO using the @Autowired annotation.

 @ContextConfiguration("classpath:testContext.xml") public class JunitDbunitTest extends AbstractTransactionalJUnit4SpringContextTests { @Autowired public PersonDao personDao; @Test public void test1() { personDao.save(new Person("")); personDao.save(new Person("")); personDao.save(new Person("")); } } 


The base class AbstractTransactionalJUnit4SpringContextTests is configured so that each test method runs in a transaction that rolls back when the method ends.

Next, you need to check that the data is really preserved in the database. You can inject into the test class EntityManager and right after inserting the data, use it to check the corresponding assertions. In most cases, when writing a DAO, this will be quite enough to control the correctness of mappings and DAO logic. The transaction at the end of the test will be rolled back, and the condition for the absence of test side effects will be met.

However, there are situations when we still need to confirm the transaction at the end of the test, make sure it is completed correctly and check what exactly is stored in the database - up to the fields of specific tables. I note that such tests are likely to tightly tie the test to the database physics and significantly increase its sensitivity. In addition, it may not be executed in conjunction with another personal service provider due to differences in the structure or naming of the tables.

The base test classes provided by Spring offer only the ability to run some SQL scripts on the test database. Consider how DBUnit can help us, and how to integrate it with the Spring TestContext Framework.

DBUnit allows you to describe the state of the database without being tied to physical data types - as an XML data set. Here is the initial data set for our test: it is empty, it declares the only table of persons corresponding to our domain class.

 <!DOCTYPE dataset SYSTEM "dataset.dtd"> <dataset> <table name="person"> <column>id</column> <column>name</column> </table> </dataset> 


But the expected data set: the persons table here contains three entries.

 <!DOCTYPE dataset SYSTEM "dataset.dtd"> <dataset> <table name="person"> <column>id</column> <column>name</column> <row> <value>1</value> <value></value> </row> <row> <value>2</value> <value></value> </row> <row> <value>3</value> <value></value> </row> </table> </dataset> 


It should be said that in DBUnit there is also an abbreviated version of the record, in which the table name is described by a tag, and the field values ​​are attributes. But the full-size format in some cases is more functional.

Create an annotation for the test method, indicating which data set should be loaded before the method starts (the before attribute), and which data set to verify the database after its completion (after attribute):

 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DbunitDataSets { String before(); String after(); } 


To handle this annotation, let's extend the standard test class AbstractTransactionalJUnit4SpringContextTests.

 @TestExecutionListeners( AbstractDbunitTransactionalJUnit4SpringContextTests.DbunitTestExecutionListener.class ) public abstract class AbstractDbunitTransactionalJUnit4SpringContextTests extends AbstractTransactionalJUnit4SpringContextTests { /**  DBUnit */ private IDatabaseTester databaseTester; /**       */ private String afterDatasetFileName; /** ,      :   */ @AfterTransaction public void assertAfterTransaction() throws Exception { if (databaseTester == null || afterDatasetFileName == null) { return; } IDataSet databaseDataSet = databaseTester.getConnection().createDataSet(); IDataSet expectedDataSet = new XmlDataSet(ClassLoader.getSystemResourceAsStream(afterDatasetFileName)); Assertion.assertEquals(expectedDataSet, databaseDataSet); databaseTester.onTearDown(); } private static class DbunitTestExecutionListener extends AbstractTestExecutionListener { /** ,     :  */ public void beforeTestMethod(TestContext testContext) throws Exception { AbstractDbunitTransactionalJUnit4SpringContextTests testInstance = (AbstractDbunitTransactionalJUnit4SpringContextTests) testContext.getTestInstance(); Method method = testContext.getTestMethod(); DbunitDataSets annotation = method.getAnnotation(DbunitDataSets.class); if (annotation == null) { return; } DataSource dataSource = testContext.getApplicationContext().getBean(DataSource.class); IDatabaseTester databaseTester = new DataSourceDatabaseTester(dataSource); databaseTester.setDataSet( new XmlDataSet(ClassLoader.getSystemResourceAsStream(annotation.before()))); databaseTester.onSetup(); testInstance.databaseTester = databaseTester; testInstance.afterDatasetFileName = annotation.after(); } } } 


The static nested class DbunitTestExecutionListener extends the AbstractExecutionListener listener - part of the TestContext framework. It is included in the test life cycle using the @TestExecutionListeners annotation on the test class.

The test class is connected to the life cycle of the test at two points. The first is the DbunitTestExecutionListener # beforeTestMethod method, which runs before each test method. In it, the listener checks for the presence of our @DbunitDataSets annotation on the current test method. If there is an annotation, it initializes the database tester from the DBUnit framework. The name of the file with the dataset to be loaded into the database before the test starts is obtained from the before field of the @DbunitDataSets annotation. The value of the after field annotations and the tester instance are stored in the fields of the test class.

The second entry point is the assertAfterTransaction () method, annotated with @AfterTransaction, which is also part of the TestContext framework. This annotation provides for the execution of the method upon the completion of the transaction of each test method, annotated with @Transactional annotation. In this method, we use the previously saved databaseTester and afterDatasetFileName, as well as standard DBUnit functionality to compare the state of the database with the expected one.

Let's see how our test will now look:

 @ContextConfiguration("classpath:testContext.xml") public class JunitDbunitTest extends AbstractDbunitTransactionalJUnit4SpringContextTests { @Autowired private PersonDao personDao; @Test @Rollback(false) @DbunitDataSets(before = "initialDataset.xml", after = "expectedDataset.xml") @DirtiesContext public void test1() { personDao.save(new Person("")); personDao.save(new Person("")); personDao.save(new Person("")); } } 


Rollback annotation (false) confirms the transaction at the end of the test. The @DirtiesContext annotation indicates the need to re-create the Spring context before the next test in the class. In our @DbunitDataSets annotation, we specify the names of files containing the initial and expected DBUnit data sets.

The limitation of this test case is the need to recreate the Spring context before each test method. At the end of the test, not only operational data remains in the database (which both DBUnit and the AbstractTransactionalJUnit4SpringContextTests # deleteFromTables method can easily delete), but also auxiliary tables and persistent provider sequences. Thus, each test method must be annotated with @DirtiesContext. In this case, a Spring context will be recreated before each test method and the database schema will be exported.

You can, in order not to waste time on raising the context, try to re-export the Hibernate scheme in Before , then you will be able to do without the @DirtiesContext annotations. But I did not do this in the base test class, first of all, so as not to bind it to Hibernate. And then, even such a hard cleaning of the database would not give confidence in getting rid of all possible side effects, such as caching.

In conclusion, I want to note that the abstract test on the basis of TestNG is written similarly and is no different from the test on the basis of JUnit, except for the extensible base class - in this case it will be AbstractTransactionalTestNGSpringContextTests. For my purposes, I brought the DbunitTestExecutionListener to a separate class and implemented two base classes for these two test frameworks.

The source code for the article is posted on GitHub: github.com/forketyfork/spring-dao-test-demo

Source: https://habr.com/ru/post/140658/


All Articles