Effective Unit Testing with DbUnit[ 转载 ]
来源:互联网 发布:娱乐贵公子 炫浪网络 编辑:程序博客网 时间:2024/04/29 11:15
01/21/2004
Introducing DbUnit
Writing unit and component tests for objects with external dependencies, such as databases or other objects, can prove arduous, as those dependencies may hinder isolation. Ultimately, effective white-box tests isolate an object by controlling outside dependencies, so as to manipulate its state or associated behavior.
Utilizing mock objects or stubs is one strategy for controlling outside dependencies. Stubbing out associated database access classes, such as those found inJDBC
, can be highly effective; however, the mock object solution may not be possible in application frameworks where the underlying database access objects may be hidden, such as those utilizingEJB
s with container-managed persistence (CMP) or Java Data Objects (JDO).
The open source DbUnit framework, created by Manuel Laflamme, provides an elegant solution for controlling a database dependency within applications by allowing developers to manage the state of a database throughout a test. With DbUnit, a database can be seeded with a desired data set before a test; moreover, at the completion of the test, the database can be placed back into its pre-test state.
Automated tests are a critical facet of most successful software projects. DbUnit allows developers to create test cases that control the state of a database during their life cycles; consequently, those test cases are easily automatable, as they do not require manual intervention between tests; nor do they entail manual interpretation of results.
Getting Started
The first step in configuring DbUnit involves the generation of a database schema definition file. This file is an XML representation of database tables and the data found in them.
For example, a database table EMPLOYEE
would be described in SQL
as follows:
Moreover, a sample data set found in EMPLOYEE
could be:
DbUnit's representation of the table and the sample data in XML would then become:
<EMPLOYEE employee_uid='1' start_date='2001-11-01' first_name='Andrew' ssn='xxx-xx-xxxx' last_name='Glover' />
This generated XML file becomes the sample template for any seed files utilized in an application.
Creating multiple seed files for associated test scenarios can be an effective strategy, as one can segregate database states via the different database files. The multiple-seed-file strategy allows for the creation of succinct, targeted data files for specific database tables, rather than the database as a whole.
To seed the target database with three different employees, the XML representation would be as follows:
<?xml version='1.0' encoding='UTF-8'?><dataset> <EMPLOYEE employee_uid='1' start_date='2001-01-01' first_name='Drew' ssn='000-29-2030' last_name='Smith' /> <EMPLOYEE employee_uid='2' start_date='2002-04-04' first_name='Nick' ssn='000-90-0000' last_name='Marquiss' /> <EMPLOYEE employee_uid='3' start_date='2003-06-03' first_name='Jose' ssn='000-67-0000' last_name='Whitson' /></dataset>
With DbUnit configured to work with the desired database schema, developers have two options for employing DbUnit in testing: in code or throughant
.
DbUnit in Code
The DbUnit framework provides a base abstract test-case class, which extends JUnit's TestCase
and is called DatabaseTestCase
. Think of this class as a template pattern for which one must provide implementations of two hook methods:getConnection()
and getDataSet()
.
The getConnection()
method expects the creation of an IDatabaseConnection
object, which wraps a normalJDBC
connection. For example, the code below demonstrates the creation of anIDatabaseConnection
for a MySQL database.
protected IDatabaseConnection getConnection() throws Exception { Class driverClass = Class.forName("org.gjt.mm.mysql.Driver"); Connection jdbcConnection = DriverManager.getConnection( "jdbc:mysql://127.0.0.1/hr", "hr", "hr"); return new DatabaseConnection(jdbcConnection);}
The getDataSet()
method expects the creation of an IDataSet
object, which is essentially a representation of a seed file containing theXML
described earlier.
protected IDataSet getDataSet() throws Exception { return new FlatXmlDataSet( new FileInputStream("hr-seed.xml"));}
With those two methods defined, DbUnit can function with default behavior; however, theDatabaseTestCase
class provides two fixture methods that control the state of the database before and after a test:getSetUpOperation()
and getTearDownOperation()
.
An effective strategy is to have the getSetUpOperation()
perform aREFRESH
operation, which updates the desired database with the data found in the seed file. Consequently, thegetTearDownOperation()
performs a NONE
operation.
protected DatabaseOperation getSetUpOperation() throws Exception { return DatabaseOperation.REFRESH; }protected DatabaseOperation getTearDownOperation() throws Exception { return DatabaseOperation.NONE;}
Another effective approach is to have the getSetUpOperation()
method perform aCLEAN_INSERT
, which deletes all data found in tables specified in the seed file and then inserts the file's data. This tactic provides precision control of a database.
Code Example
In a J2EE human resources application, we would like to automate a series of test cases for a Session Façade that handles employee creation, retrieval, updating, and deletion. The remote interface contains the following business methods (thethrows
clauses are removed for brevity's sake):
public void createEmployee( EmployeeValueObject emplVo )public EmployeeValueObject getEmployeeBySocialSecNum( String ssn )public void updateEmployee( EmployeeValueObject emplVo )public void deleteEmployee( EmployeeValueObject emplVo )
Testing the getEmployeeBySocialSecNum()
method would require seeding the database with an employee record. Additionally, testing thedeleteEmployee()
and updateEmployee()
would also depend on a previously created database record. Lastly, the test suite will create an employee from scratch, verifying that no exceptions were generated, by utilizing thecreateEmployee()
method.
The following DbUnit seed file, named employee-hr-seed.xml
, will be utilized:
<?xml version='1.0' encoding='UTF-8'?><dataset> <EMPLOYEE employee_uid='1' start_date='2001-01-01' first_name='Drew' ssn='333-29-9999' last_name='Smith' /> <EMPLOYEE employee_uid='2' start_date='2002-04-04' first_name='Nick' ssn='222-90-1111' last_name='Marquiss' /> <EMPLOYEE employee_uid='3' start_date='2003-06-03' first_name='Jose' ssn='111-67-2222' last_name='Whitson' /></dataset>
The test suite, EmployeeSessionFacadeTest
, will extend DbUnit's DatabaseTestCase
and provide implementations for both the getConnection()
andgetDataSet()
methods, where the getConnection()
method obtains a connection to the same database instance theEJB
container is utilizing, and the getDataSet()
method reads in the aboveemployee-hr-seed.xml file.
The test methods are quite simple, as DbUnit handles the complex database lifecycle tasks for us. To test thegetEmployeeBySocialSecNum()
method, simply pass in a social security number from the seed file, such as "333-29-9999."
public void testFindBySSN() throws Exception{ EmployeeFacade facade = //obtain somehow EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("333-29-9999"); TestCase.assertNotNull("vo shouldn't be null", vo); TestCase.assertEquals("should be Drew", "Drew", vo.getFirstName()); TestCase.assertEquals("should be Smith", "Smith", vo.getLastName());}
Ensuring the façade's create method works properly is as easy as executing a create
operation and verifying that no exceptions were thrown. Additionally, the next step could be to attempt afind
operation on the newly created entity.
public void testEmployeeCreate() throws Exception{ EmployeeValueObject empVo = new EmployeeValueObject(); empVo.setFirstName("Noah"); empVo.setLastName("Awan"); empVo.setSSN("564-55-5555"); EmployeeFacade empFacade = //obtain from somewhere empFacade.createEmployee(empVo); //perform a find by ssn to ensure existence}
Testing updateEmployee()
involves four steps. First find the desired employee entity, and then update the object. Next, re-find the same entity and test to ensure that the updated values are properly reflected.
public void testUpdateEmployee() throws Exception{ EmployeeFacade facade = //obtain façade EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("111-67-2222"); TestCase.assertNotNull("vo was null", vo); TestCase.assertEquals("first name should be Jose", "Jose", vo.getFirstName()); vo.setFirstName("Ramon"); facade.updateEmployee(vo); EmployeeValueObject newVo = facade.getEmployeeBySocialSecNum("111-67-2222"); TestCase.assertNotNull("vo was null", newVo); TestCase.assertEquals("name should be Ramon", "Ramon", newVo.getFirstName());}
Guaranteeing the façade's deletion function properly works is similar to the testUpdateEmployee()
method, as there are three steps: find an existing entity, remove it, and then attempt to find it again, verifying that no entity could be found.
public void testDeleteEmployee() throws Exception{ EmployeeFacade facade = //obtain façade EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("222-90-1111"); TestCase.assertNotNull("vo was null", vo); facade.deleteEmployee(vo); try{ EmployeeValueObject newVo = facade.getEmployeeBySocialSecNum("222-90-1111"); TestCase.fail("returned removed employee"); }catch(Exception e){ //ignore }}
The test suite code is simple and easy to follow, as the code focuses on testing the desired object and not on any assorted plumbing code to facilitate the test. Additionally, the test case is easily automated.
DbUnit in Ant
Rather than extending DbUnit's DatabaseTestCase
, the DbUnit framework comes with anant
task, which allows the control of a database within anAnt
build file. The task is quite powerful, as it provides a simplistic declarative strategy for test cases. For example, runningJUnit
tests in Ant
is as easy as defining a task as follows:
<junit printsummary="yes" haltonfailure="yes"> <formatter type="xml"/> <batchtest fork="yes" todir="${reports.tests}"> <fileset dir="${src.tests}"> <include name="**/*Test.java"/> </fileset> </batchtest></junit>
With DbUnit's task, controlling the state of the database before and after theJUnit
task involves creating a "setup" operation, in which the seed file's contents are inserted into a target database:
<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask"/><dbunit driver=" org.gjt.mm.mysql.Driver " url=" jdbc:mysql://127.0.0.1/hr " userid="hr" password="hr"> <operation type="INSERT" src="seedFile.xml"/></dbunit>
And a "tear down" operation, in which the same data is deleted from the target database:
<dbunit driver=" org.gjt.mm.mysql.Driver " url=" jdbc:mysql://127.0.0.1/hr " userid="hr" password="hr"> <operation type="DELETE" src="seedFile.xml"/></dbunit>
Wrapping the JUnit
task with the above operations effectively loads the target database before the batch test executes and then deletes all loaded data when the tests complete.
<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask"/><!-- set up operation --><dbunit driver=" org.gjt.mm.mysql.Driver " url=" jdbc:mysql://127.0.0.1/hr " userid="hr" password="hr"> <operation type="INSERT" src="seedFile.xml"/></dbunit><!-- run all tests in the source tree --><junit printsummary="yes" haltonfailure="yes"> <formatter type="xml"/> <batchtest fork="yes" todir="${reports.tests}"> <fileset dir="${src.tests}"> <include name="**/*Test*.java"/> </fileset> </batchtest></junit><!-- tear down operation --><dbunit driver=" org.gjt.mm.mysql.Driver " url=" jdbc:mysql://127.0.0.1/hr " userid="hr" password="hr"> <operation type="DELETE" src="seedFile.xml"/></dbunit>
Conclusion
The DbUnit framework's ability to manage the state of a database throughout a test's lifecycle enables rapid test-case creation and adoption; furthermore, by controlling a major dependency, tests that utilize the DbUnit framework are easily automated.
DbUnit's elegant design makes learning how to properly utilize its features a breeze. Once it's in place as a part of an effective testing strategy, overall code stability will increase dramatically, along with the collective confidence of your development team.
- Effective Unit Testing with DbUnit[ 转载 ]
- Unit testing with CPPUnit
- Unit Testing with wxPython
- Unit testing with R
- Unit Testing with CPPUnit(ZZ)
- Multithreaded unit testing with ConTest
- IOS Unit Testing With OCMock
- Unit Testing with Android Studio
- Unit Testing with JUnit - Tutorial
- Unit Testing With Android Studio
- Unit Testing with Sinon.JS
- Unit Testing in C# with Nunit
- Pragmatic Unit Testing in Java with JUnit
- Unit Testing DAO Classes with JUnit, Spring
- Java Unit Testing with JUnit in NetBeans
- Testing SQL queries with Spring and DbUnit, part 1
- Unit Testing
- unit testing
- 最长公共子字符串
- 闰年的计算
- 进程和线程
- Maven依赖jar包的查询
- 无限级分类(非递归算法/存储过程版/GUID主键)完整数据库示例_(3)删除记录
- Effective Unit Testing with DbUnit[ 转载 ]
- 求AOE网的关键路径
- JAVA解析JSON数据代码
- 角色权限
- GridView+Page(Gidview及分页)
- uCLinux LINUX区别
- Android 启动activity 空白页面
- 无限级分类(非递归算法/存储过程版/GUID主键)完整数据库示例_(2)插入记录
- 搭建Android生产环境傻瓜教程(一)Ubuntu系统环境和常用软件