TestNG Spring Integration Example
In this article, we will go through a few examples of TestNG and spring integration. Before we start with the examples, let me first brief you on the goal of spring’s integration testing framework:
- Manage Spring IoC container caching between test execution.
- Provide Dependency Injection of test fixture instances.
- Provide transaction management appropriate to integration testing.
In this article, first I will brief you on Spring TestContext
Framework and then I will be showing an example for each of the above case using TestNG as the testing framework.
Below are my setup details:
- I am using Eclipse as the IDE, version Luna 4.4.1.
- I will be running the tests using eclipse TestNG plugin so you need to install the TestNG Eclipse Plugin.
- Since the project depends on spring, TestNG, MySql for the database, we will be creating a Maven based project in eclipse. If you are new to Maven, you may go through the details here.
1. Spring TestContext Framework
In Spring 2.5 and later, unit and integration testing support is provided in the form of the annotation-driven Spring TestContext
Framework. The TestContext
framework is agnostic of the actual testing framework in use so irrespective of the framework, on each test execution event, the task to be performed is delegated to corresponding method in TestContextManager.
That in turn takes care of loading and accessing ApplicationContext
, caching it, dependency injection of test instances and transactional execution of test methods, etc.
Even though TestContextManager
takes care of the test integration responsibility, there is still a layer that binds the testing framework to the spring’s integration testing framework and this layer consists of a couple of abstract support classes.
AbstractTestNGSpringContextTests
– Integrates the SpringTestContext
Framework with explicitApplicationContext
which is accessible to sub-classes as protected member.AbstractTransactionalTestNGSpringContextTests
– This extendsAbstractTestNGSpringContextTests
. It not only provides transactional support but also has some convenience functionality for JDBC access.
2. Add TestNG and Spring Dependencies to pom.xml
Our project is dependent on the below modules:
spring-context
– we will be loadingApplicationContext.
spring-test
– to access spring’s testing framework.spring-jdbc
– forJdbcTemplate
andDataSource
support.mysql-connector-java
– MySql driver.testng
– as this is our testing tool.
With the above dependencies, our pom.xml
looks like below:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javacodegeeks.testng.spring</groupId> <artifactId>testNGSpring</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.26</version> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.8.8</version> <scope>test</scope> </dependency> </dependencies> <properties> <spring.version>4.1.5.RELEASE</spring.version> </properties> </project>
3. Example of TestNG and Spring Dependency Injection
In this example, we will test our simple Foo
bean. We will inject this bean into our test instance and then verify its value.
Foo:
package com.javacodegeeks.testng.spring; public class Foo { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
Now define the bean in a spring XML context file and inject the name
value using the property setter.
context.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <bean id="foo" class="com.javacodegeeks.testng.spring.Foo"> <property name="name" value="TestNG Spring"/> </bean> </beans>
The test instance goes through the spring machinery for the dependency injection and the initialization of the the test instance. The factory callbacks such as setBeanName
, setBeanFactory
and the bean post processors are applied on the test instance.
- Now lets look into our test class. First thing we notice is that the test class extends
AbstractTestNGSpringContextTests
as our goal is also to make sure that the dependencies are injected into our test instance. - Context file is provided using the type annotation
@ContextConfiguration
with file namecontext.xml
as the value. Spring will use this file location to load the context which is then set to the protected memberapplicationContext
- Bean
foo
is injected using@Autowired
annotation. - The test class contains an
@Autowired
beanFactory
member. Spring automatically injectsBeanFactory
bean into it.
About test cases:
verifyFooName
– verifies that the foo bean is injected and its name is same as the one set in the context file.verifyBeanFactory
– verifies that the bean factory is injected.
SpringTestNGDependencyInjectionExample:
package com.javacodegeeks.testng.spring; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import static org.testng.Assert.*; import org.testng.annotations.Test; @ContextConfiguration("context.xml") public class SpringTestNGDependencyInjectionExample extends AbstractTestNGSpringContextTests { @Test public void verifyFooName() { System.out.println("verifyFooName: Is foo not null? " + (foo != null)); assertNotNull(foo); System.out.println("verifyFooName: Foo name is '" + foo.getName() + "'"); assertEquals(foo.getName(), "TestNG Spring"); } @Test public void verifyBeanFactory() { System.out.println("verifyBeanFactory: Is bean factory not null? " + (beanFactory!= null)); assertNotNull(beanFactory); } @Autowired private BeanFactory beanFactory; @Autowired private Foo foo; }
testng_context_dependency_injection.xml:
<?xml version="1.0" encoding="UTF-8"?> <suite name="TestNGSpringIntegration Suite" parallel="false"> <test name="TestNGSpringIntegrationTest"> <classes> <class name="com.javacodegeeks.testng.spring.SpringTestNGDependencyInjectionExample"/> </classes> </test> </suite>
Output:
[TestNG] Running: C:\javacodegeeks_ws\testNGSpring\src\test\resources\com\javacodegeeks\testng\spring\testng_context_dependency_injection.xml verifyBeanFactory: Is bean factory not null? true verifyFooName: Is foo not null? true verifyFooName: Foo name is 'TestNG Spring' =============================================== TestNGSpringIntegration Suite Total tests run: 2, Failures: 0, Skips: 0 ===============================================
4. Example of TestNG and Spring Context
Spring loads the context and caches it by default so that once loaded the test methods can directly access it from cache. This will certainly improve performance if the beans being loaded take time to initialize.
In this example, we will examine how one can override the default behavior by using the @DirtiesContext
. By default, spring caches the context, so once a test method finishes its execution, the next test method to be run uses the same context as the one before it. We can override this behavior using @DirtiesContext
. This will mark the cache as dirty and the context will be created and re-cached before the execution of the next test method.
About the methods used in the test class:
saveFooName
– This is a@BeforeClass
method. Here we will save the foo’s name so that we can compare it later.removeFromCache
– This method is annotated with@DirtiesContext
so the context in cache gets marked as dirty. Also note that the foo’s name is changed here.verifyContextNew
– This depends onremoveFromCache
so after its run, we check whether the foo’s name is still the default one or changed. SinceremoveFromCache
is annotated with@DirtiesContext
,verifyContextNew
should be getting a new context, so thefoo
bean will be the refreshed one which is why its name is still the default one set in the context file.verifyContextSame
– This method makes sure that the context is still the cached one.
SpringTestNGContextCacheExample:
package com.javacodegeeks.testng.spring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import static org.testng.Assert.*; @ContextConfiguration("context.xml") public class SpringTestNGContextCacheExample extends AbstractTestNGSpringContextTests { @BeforeClass private void saveFooName() { TestNG_Spring= foo.getName(); System.out.println("BeforeClass: foo name is '" + TestNG_Spring + "'"); assertEquals(TestNG_Spring, "TestNG Spring"); } @Test @DirtiesContext public void removeFromCache() { String newFooName = "New foo name"; foo.setName(newFooName); System.out.println("removeFromCache: foo name changed to '" + foo.getName() + "'"); this.dirtiedApplicationContext = super.applicationContext; System.out.println("removeFromCache: annotated @DirtiesContext so context will be marked for removal in afterMethod "); } @Test(dependsOnMethods = {"removeFromCache"}) public void verifyContextNew() { System.out.println("verifyContextNew: is context re-cached? " + (dirtiedApplicationContext != applicationContext)); System.out.println("verifyContextNew: foo name is '" + foo.getName() + "'"); assertNotSame(super.applicationContext, this.dirtiedApplicationContext, "The application context should have been 'dirtied'."); assertEquals(foo.getName(), TestNG_Spring); this.dirtiedApplicationContext = super.applicationContext; foo.setName(MODIFIED_FOO_NAME); System.out.println("verifyContextNew: modify foo name to '" + MODIFIED_FOO_NAME + "'"); } @Test(dependsOnMethods = { "verifyContextNew" }) public void verifyContextSame() { System.out.println("verifyContextSame: is context cached? " + (dirtiedApplicationContext == applicationContext)); assertSame(this.applicationContext, this.dirtiedApplicationContext, "The application context should NOT have been 'dirtied'."); System.out.println("verifyContextSame: foo name is '" + foo.getName() + "'"); assertEquals(foo.getName(), MODIFIED_FOO_NAME); } private String TestNG_Spring; private static final String MODIFIED_FOO_NAME = "TestNG Spring Name Changed"; private ApplicationContext dirtiedApplicationContext;; @Autowired private Foo foo; }
testng_context_cache.xml:
<?xml version="1.0" encoding="UTF-8"?> <suite name="TestNGSpringIntegration Suite" parallel="false"> <test name="TestNGSpringIntegrationTest"> <classes> <class name="com.javacodegeeks.testng.spring.SpringTestNGContextCacheExample"/> </classes> </test> </suite>
Output:
[TestNG] Running: C:\javacodegeeks_ws\testNGSpring\src\test\resources\com\javacodegeeks\testng\spring\testng_context_cache.xml BeforeClass: foo name is 'TestNG Spring' removeFromCache: foo name changed to 'New foo name' removeFromCache: annotated @DirtiesContext so context will be marked for removal in afterMethod verifyContextNew: is context re-cached? true verifyContextNew: foo name is 'TestNG Spring' verifyContextNew: modify foo name to 'TestNG Spring Name Changed' verifyContextSame: is context cached? true verifyContextSame: foo name is 'TestNG Spring Name Changed' =============================================== TestNGSpringIntegration Suite Total tests run: 3, Failures: 0, Skips: 0 ===============================================
5. TestNG Spring Transaction Integration
In this example, we will run each test method within a transaction. By default, the transaction is rolled back after the test method’s execution. One can override this behavior or explicitly commit the transaction. To run the example, we will need the support of a database. We will be using MySql as the database. Below is the schema script that we will execute during the loading of the context and so before we proceed with the first test method we will have the schema already built.
Script contains just an employee
table with one column called name
.
db-schema.sql:
drop table if exists `employee`; CREATE TABLE employee ( name VARCHAR(20) NOT NULL, PRIMARY KEY(name) );
Some data which we will execute in the test class.
data.sql:
INSERT INTO employee VALUES('Joe'); INSERT INTO employee VALUES('Sam');
Some more additional data.
additional_data.sql:
INSERT INTO employee VALUES('John');
Here is our context file. Since we are going to run our test methods within a transaction, the context file contains inject dataSource
and transactionManager
beans. In jdbc:initialize-database
, we initialize the schema.
tran_context.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd"> <jdbc:initialize-database data-source="dataSource" enabled="true"> <jdbc:script location="classpath:com/javacodegeeks/testng/spring/db-schema.sql" /> </jdbc:initialize-database> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost/test" /> <property name="username" value="root" /> <property name="password" value="mnrpass" /> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource" /> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" autowire="byName"/> </beans>
About our test class:
- The test class extends spring provided
AbstractTransactionalTestNGSpringContextTests
. This is important for the tests to automatically run within a transactional. saveMethodName
– this is a@BeforeMethod
configuration method to capture the method name which we will use later to run test based asserts.tran
– assures thatJdbcTemplate
bean is injected.beforeTransaction
– this is annotated with@BeforeTransaction
. It executes before the start of transaction. We delete all the data from employee and re-create the data.insertEmployeeAndCommit
– in this test method, we insert new employees and explicitly commit.insertEmployeeWithRollbackAsDefault
– in this test method, we insert new employees. Since by default the transaction gets rolled back after the test’s execution, we shouldn’t see the new employees in@AfterTransaction
method.insertEmployeeWithCommitAsDefault
– here we override the default behavior of rollback using the annotation@Rollback(false)
so now the transaction will automatically get committed.insertEmployeeUsingSqlAnnotation
– here we run an Sql script using@Sql
annotation. The script nameadditional_data.sql
is passed as value.afterTransaction - this is annotated with
@AfterTransaction
. Here we run all the asserts to make sure we get the behavior we are expecting.
SpringTestNGTransactionExample:
package com.javacodegeeks.testng.spring; import static org.springframework.test.context.transaction.TestTransaction.end; import static org.springframework.test.context.transaction.TestTransaction.flagForCommit; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; import org.springframework.test.context.transaction.AfterTransaction; import org.springframework.test.context.transaction.BeforeTransaction; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @ContextConfiguration("tran_context.xml") public class SpringTestNGTransactionExample extends AbstractTransactionalTestNGSpringContextTests { private String method; @BeforeMethod public void saveMethodName(Method method) { this.method = method.getName(); } @Test public void tran() { System.out.println("tran: verify JdbcTemplate is not null"); assertNotNull(jdbcTemplate); } @BeforeTransaction public void beforeTransaction() { System.out.println("before transaction starts, delete all employees and re-run employee script"); deleteFromTables("employee"); executeSqlScript("classpath:/com/javacodegeeks/testng/spring/data.sql", false); } @Test public void insertEmployeeAndCommit() { System.out.println("insertEmployeeAndCommit: insert employee 'Bill' and commit"); String emp = "Bill"; jdbcTemplate.update("insert into employee(name) values (?)", emp); assertEquals(countRowsInTableWhere("employee", "name='" + emp + "'"), 1); flagForCommit(); end(); } @Test public void insertEmployeeWithRollbackAsDefault() { System.out.println("insertEmployeeWithRollbackAsDefault: insert employee 'Bill', rollback by default"); String emp = "Bill"; jdbcTemplate.update("insert into employee(name) values (?)", emp); assertEquals(countRowsInTableWhere("employee", "name='" + emp + "'"), 1); } @Test @Rollback(false) public void insertEmployeeWithCommitAsDefault() { System.out.println("insertEmployeeWithCommitAsDefault: insert employee 'Bill', commit by default"); String emp = "Bill"; jdbcTemplate.update("insert into employee(name) values (?)", emp); assertEquals(countRowsInTableWhere("employee", "name='" + emp + "'"), 1); } @Test @Sql({"additional_data.sql"}) public void insertEmployeeUsingSqlAnnotation() { System.out.println("insertEmployeeUsingSqlAnnotation: run additional sql using @sql annotation, rollback by default"); assertEquals(countRowsInTableWhere("employee", "name='John'"), 1); } @AfterTransaction public void afterTransaction() { switch (method) { case "insertEmployeeAndCommit": assertEmployees("Bill", "Joe", "Sam"); System.out.println("insertEmployeeAndCommit: employees found: 'Bill', 'Joe' and 'Sam'"); break; case "insertEmployeeWithRollbackAsDefault": System.out.println("insertEmployeeWithRollbackAsDefault: employees found: 'Joe' and 'Sam'"); assertEmployees("Joe", "Sam"); break; case "insertEmployeeWithCommitAsDefault": System.out.println("insertEmployeeWithCommitAsDefault: employees found: 'Bill', 'Joe' and 'Sam'"); assertEmployees("Bill", "Joe", "Sam"); break; case "tran": break; case "insertEmployeeUsingSqlAnnotation": System.out.println("insertEmployeeWithRollbackAsDefault: employees found: 'Joe' and 'Sam'"); assertEmployees("Joe", "Sam"); break; default: throw new RuntimeException( "missing 'after transaction' assertion for test method: " + method); } } private void assertEmployees(String... users) { List expected = Arrays.asList(users); Collections.sort(expected); List actual = jdbcTemplate.queryForList("select name from employee", String.class); Collections.sort(actual); System.out.println("Employees found: " + actual); assertEquals(expected, actual); } @Autowired private JdbcTemplate jdbcTemplate; }
testng_spring_transaction.xml:
<?xml version="1.0" encoding="UTF-8"?> <suite name="TestNGSpringIntegration Suite" parallel="false"> <test name="TestNGSpringIntegrationTest"> <classes> <class name="com.javacodegeeks.testng.spring.SpringTestNGTransactionExample"/> </classes> </test> </suite>
Output:
[TestNG] Running: C:\javacodegeeks_ws\testNGSpring\src\test\resources\com\javacodegeeks\testng\spring\testng_spring_transaction.xml before transaction starts, delete all employees and re-run employee script insertEmployeeAndCommit: insert employee 'Bill' and commit Employees found: [Bill, Joe, Sam] insertEmployeeAndCommit: employees found: 'Bill', 'Joe' and 'Sam' before transaction starts, delete all employees and re-run employee script insertEmployeeUsingSqlAnnotation: run additional sql using @sql annotation, rollback by default insertEmployeeWithRollbackAsDefault: employees found: 'Joe' and 'Sam' Employees found: [Joe, Sam] before transaction starts, delete all employees and re-run employee script insertEmployeeWithCommitAsDefault: insert employee 'Bill', commit by default insertEmployeeWithCommitAsDefault: employees found: 'Bill', 'Joe' and 'Sam' Employees found: [Bill, Joe, Sam] before transaction starts, delete all employees and re-run employee script insertEmployeeWithRollbackAsDefault: insert employee 'Bill', rollback by default insertEmployeeWithRollbackAsDefault: employees found: 'Joe' and 'Sam' Employees found: [Joe, Sam] before transaction starts, delete all employees and re-run employee script =============================================== TestNGSpringIntegration Suite Total tests run: 5, Failures: 0, Skips: 0 ===============================================
Download the Eclipse Project
In this article, I have shown you examples of TestNG Spring Integration.
You can download the full source code of this example here: testNGSpring.zip