Effective Unit Testing with DbUnit[ 转载 ]

来源:互联网 发布:娱乐贵公子 炫浪网络 编辑:程序博客网 时间:2024/04/29 11:15
by Andrew Glover
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 utilizingEJBs 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:

EMPLOYEE database table

Moreover, a sample data set found in EMPLOYEE could be:

EMPLOYEE sample data

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.

原创粉丝点击