Unit-testing database applications 无论软件开发规模,依赖关系都是软件开发的关键问题……去除程序中的重复部分,也就去除了依赖性。
——Kent Beck
本章内容
本章的目的就是告诉我们,不仅对数据库访问代码进行单元测试是可行的,而且还有很多的解决方案,在探究完对数据库进行单元测试的种种方法之后,将启示一些准则,帮助你决定你的特定应用程序到底选用哪种方法!
1.对数据库进行单元测试的介绍
(图片备用链接)
在前面的三个笔记中,我们描述了如何对JSP和filter进行单元测试,而今天针对的部分就是图中黄色区域了!
这里我们重点学习如何对JDBC组件进行单元测试。
这里假设你的数据库访问代码已经清晰的与你的业务逻辑代码区分了。在此处分离关注焦点是个很好的做法,在不改变其他部分代码的情况下,使得测试变得简单了!
2.隔离开数据库测试业务逻辑
这里我们的目标是对不包含数据库访问代码的业务逻辑代码进行单元测试。尽管这种测试本质上说不是数据库测试,但它却是独立的测试那些难以测试的数据库代码的一个很好的策略。幸运的是,如果你将数据库的访问层和业务逻辑层分开的话,这项工作将变得非常的简单。让我们看看这意味着什么。AdminServlet的定义如下:
package junitbook.database; import java.util.Collection; import javax.naming.NamingException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; public class AdminServlet extends HttpServlet { public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException { } public String getCommand(HttpServletRequest request ) throws ServletException { } public Collection executeCommand(String command) throws Exception { } }
在这种方法下程序执行的逻辑图如下:
接下来的诀窍就是为数据库访问层建立一个接口,有了接口,你就可以用mock objects策略来对数据库访问层进行单元测试。
实现数据库访问层的接口
我们将此接口称为DataAccessManager,并把它的实现称为JdbcDataAccessManager。
package junitbook.database; import java.util.Collection; public interface DataAccessManager { Collection execute(String sql) throws Exception; }
既然已经有了数据访问的接口,你需要重构类AdminServlet以使用该接口并实例化DataAccessManager的JdbcDataAccessManager的实现。
下面的代码展示了重构的结果:
package junitbook.database; import java.util.Collection; import javax.naming.NamingException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; public class AdminServlet extends HttpServlet { // [...] private DataAccessManager dataManager; public void init() throws ServletException { super.init(); try { setDataAccessManager(new JdbcDataAccessManager()); } catch (NamingException e) { throw new ServletException(e); } } public Collection executeCommand(String command) throws Exception { return this.dataManager.execute(command); } }
现在为AdminServlet类的一个方法写一个单元测试就很简单了。你所要做的就是建立一个DataAccessManager的mock object实现。唯一需要技巧的地方就是决定如何将mock实例传给AdminServlet类以使AdminServlet类使用mock实现,而不是真实的JdbcDataAccessManager的实现。
建立一个模拟数据库接口
你可以采用多种策略来将一个DataAccessManager的mock 传给AdminServlet:
对于上面的方法,最好的方法就是使用setter方法了。所以,再次的将AdminServlet重构如下:
package junitbook.database; import java.util.Collection; import javax.naming.NamingException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; public class AdminServlet extends HttpServlet { // [...] private DataAccessManager dataManager; public DataAccessManager getDataAccessManager() { return this.dataManager; } public void setDataAccessManager(DataAccessManager manager) { this.dataManager = manager; } public void init() throws ServletException { super.init(); try { setDataAccessManager(new JdbcDataAccessManager()); } catch (NamingException e) { throw new ServletException(e); } } public Collection executeCommand(String command) throws Exception { return this.dataManager.execute(command); } }
模拟数据库接口层
使用test case如何创建并使用mock DataAccessManager类来对AdminServlet类进行单元测试呢?下面就是使用DynaMock API写的mock例子。
package junitbook.database; import java.util.ArrayList; import com.mockobjects.dynamic.Mock; import com.mockobjects.dynamic.C; import junit.framework.TestCase; public class TestAdminServletDynaMock extends TestCase { public void testSomething() throws Exception { Mock mockManager = new Mock(DataAccessManager.class); DataAccessManager manager = (DataAccessManager) mockManager.proxy(); mockManager.expectAndReturn("execute", C.ANY_ARGS, new ArrayList()); AdminServlet servlet = new AdminServlet(); servlet.setDataAccessManager(manager); // Call the method to test here. For example: // manager.doGet(request, response) // [...] } }
首先用DynaMock API创建一个DataAccessManager mock object,接下来当调用execute方法时你让该mock返回一个空的ArrayList。接着你要setDataAccessManager方法建立一个mock管理器。
3.隔离开数据库测试持久性代码
前面的章节将业务层和数据访问层进行隔离测试,接下来,就将对数据访问层进行测试。
首先看一下将要被测试的代码:
package junitbook.database; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import org.apache.commons.beanutils.RowSetDynaClass; public class JdbcDataAccessManager implements DataAccessManager { private DataSource dataSource; public JdbcDataAccessManager() throws NamingException { this.dataSource = getDataSource(); } protected DataSource getDataSource() throws NamingException { InitialContext context = new InitialContext(); DataSource dataSource = (DataSource) context.lookup("java:/DefaultDS"); return dataSource; } protected Connection getConnection() throws SQLException { return this.dataSource.getConnection(); } public Collection execute(String sql) throws Exception { Connection connection = getConnection(); // For simplicity, we'll assume the SQL is a SELECT query ResultSet resultSet = connection.createStatement().executeQuery(sql); RowSetDynaClass rsdc = new RowSetDynaClass(resultSet); resultSet.close(); connection.close(); return rsdc.getRows(); } }
正如你所见,execute方法很简单。这种简单源于BeanUtils包的使用。BeanUtils提供了一个RowSetDynaClass,它封装了一个ResultSet并将数据库各列映射到bean属性中。然后你就可以将各列作为属性用DynaBean API来访问了。
类RowSetDynaClass自动将ResultSet各列拷贝到dyna bean的属性中,这就使得你可以在一结束初始化RowSetDynaClass对象后就关闭与数据库的链接。
测试execute方法
让我们为execute方法写单元测试,就是对所有的JDBC的调用提供mock。
首先,将一个mock Connection对象传递给JdbcDataAccessManager类。在这里,我们创建一个封装类。你可以将getConnection方法定义为保护成员,接着创建一个派生自JdbcDataAccessManager 的新类TestableJdbcDataAccessManager。并添加setter方法,这样就绕过了DataSource而获得连接。
package junitbook.database; import java.sql.Connection; import java.sql.SQLException; import javax.naming.NamingException; import javax.sql.DataSource; public class TestableJdbcDataAccessManager extends JdbcDataAccessManager { private Connection connection; public TestableJdbcDataAccessManager() throws NamingException { super(); } public void setConnection(Connection connection) { this.connection = connection; } protected Connection getConnection() throws SQLException { return this.connection; } protected DataSource getDataSource() throws NamingException { return null; } }
创建第一个测试
现在我们有了自己的方式来编写execute方法,所以,开始为它编写第一个测试程序。对于任何使用mocks的测试来说,困难的地方在于找到那些需要模拟的方法。换句话说,为了提供模拟的响应你必须准确的理解API的哪些方法将被调用。通常,你可以尝试犯些错误,接着测试运行,一步步重构。
package junitbook.database; import java.util.Collection; import java.util.Iterator; import org.apache.commons.beanutils.DynaBean; import com.mockobjects.sql.MockConnection2; import com.mockobjects.sql.MockStatement; import junit.framework.TestCase; public class TestJdbcDataAccessManagerMO1 extends TestCase { private MockStatement statement; private MockConnection2 connection; private TestableJdbcDataAccessManager manager; protected void setUp() throws Exception { statement = new MockStatement(); 创建Statement和Connection mock objects connection = new MockConnection2(); connection.setupStatement(statement); 让Connection mock 返回mock Statement对象 manager = new TestableJdbcDataAccessManager(); 实例化该封装类,并调用setConnection()方法来传递mock Connection对象 manager.setConnection(connection); } public void testExecuteOk() throws Exception { String sql = "SELECT * FROM CUSTOMER"; Collection result = manager.execute(sql); 调用方法进行单元测试 Iterator beans = result.iterator(); 通过返回的Collection来判断结果 assertTrue(beans.hasNext()); DynaBean bean1 = (DynaBean) beans.next(); assertEquals("John", bean1.get("firstname")); assertEquals("Doe", bean1.get("lastname")); assertTrue(!beans.hasNext()); } }
这个程序并没有结束,你会发现一个错误,你还没有告诉但executeQuery方法被调用时mock Statement应该返回些什么!
改进该测试
package junitbook.database; import java.util.Collection; import java.util.Iterator; import org.apache.commons.beanutils.DynaBean; import com.mockobjects.sql.MockConnection2; import com.mockobjects.sql.MockSingleRowResultSet; import com.mockobjects.sql.MockStatement; import junit.framework.TestCase; public class TestJdbcDataAccessManagerMO2 extends TestCase { private MockSingleRowResultSet resultSet; private MockStatement statement; private MockConnection2 connection; private TestableJdbcDataAccessManager manager; protected void setUp() throws Exception { resultSet = new MockSingleRowResultSet(); statement = new MockStatement(); connection = new MockConnection2(); connection.setupStatement(statement); manager = new TestableJdbcDataAccessManager(); manager.setConnection(connection); } public void testExecuteOk() throws Exception { String sql = "SELECT * FROM CUSTOMER"; statement.addExpectedExecuteQuery(sql, resultSet); String[] columnsLowercase = new String[] {"firstname", "lastname"}; resultSet.addExpectedNamedValues(columnsLowercase, new Object[] {"John", "Doe"}); Collection result = manager.execute(sql); Iterator beans = result.iterator(); assertTrue(beans.hasNext()); DynaBean bean1 = (DynaBean) beans.next(); assertEquals("John", bean1.get("firstname")); assertEquals("Doe", bean1.get("lastname")); assertTrue(!beans.hasNext()); } }
这里我们使用了MockSingeRowResultSet实现。MockObjects.com提供了两种实现,MockSingleRowResultSet和MockMultiRowResultSet。顾名思义
:第一种是用来模拟只有一行的ResultSet,而第二种方法模拟具有多行的情况。
该测试同样失败了。。。但是,我们取得了很多的进步,这里怎样改进在下一笔记中解决。