我遇到了模拟数据访问逻辑的问题。
我正在使用JavaEE,Struts和我的自定义数据访问逻辑开发Web应用程序。在此应用程序中,Struts Action与UserDao一起操作以检索User对象。 UserDao对象的生命周期与JDBC事务相关联。想法是当Action创建UserDao对象时它启动JDBC事务(必要的JDBC东西存储在UserDao对象中),UserDao方法的所有调用都在单个JDBC事务中运行,然后Action终止UserDao对象完成事务(使用commit或rollback)。 / p>
问题是在测试动作期间我需要模拟这个UserDao,使其返回带有必要测试数据的User对象。
到目前为止我找到的唯一解决方案是可怕的。我做了以下内容:将UserDao拆分为UserDao接口和使用它实现它的UseDaoImpl类。此接口也将由UserDaoMock实现,它将返回必要的测试数据。接下来我需要一些结构,它将在生产运行中返回真正的UserDao(UserDaoImpl)并在测试运行中返回mock(UserDaoMock)。此结构不应依赖于UserDaoMock类型来使应用程序的生产代码独立于mock .class文件。这导致了糟糕的设计。
缺点:
在面料中:
我需要在Fabric中使用UserDao的一个实例才能调用instantiate
方法(实际上是复制构造函数,但我需要它是一个能够使用多态来选择的方法)在UserDaoImpl或UserDaoMock之间创建另一个UserDao对象,它将与事务关联(取决于运行时类型,它将是UserDaoImpl或UserDaoMock的instantiate
方法。)
private static UserDaoFabric instance;
private UserDao exampleUserDao;
public static UserDaoFabric getInstance() {
if(instance == null) {
instance = new UserDaoFabric();
// By default we initialize fabric with example of real UserDao.
// In test will be set to example of UserDaoMock with setExampleUserDao method.
instance.setExampleUserDao(new UserDaoImpl(true));
}
return instance;
}
public UserDao getUserDao() {
return exampleUserDao.instantiate(true);
}
public void setExampleUserDao(UserDao userDao) {
this.exampleUserDao = userDao;
}
}
在UserDao中:
instantiate
方法(实际上是复制构造函数)。构造函数应该有参数,告诉它是否是示例(是否启动事务)。
public final class UserDaoImpl implements UserDao {
private DataSource ds;
private Connection conn;
public UserDao instantiate(boolean startTransaction) {
if(startTransaction) {
return new UserDaoImpl(true);
} else {
return new UserDaoImpl(false);
}
}
public UserDaoImpl(boolean initiate) {
if(initiate) {
// DB connection initialization and start of the transaction
} else {
new UserDaoImpl();
}
}
private UserDaoImpl() {
}
@Override
public void terminate(boolean commit) {
// End of the transaction and DB connection termination
}
// Other interface methods implementation ommited
}
在这种情况下,有没有办法为可测试性做出正确的设计?
或者,如果没有,问题的原因可能是我决定将UserDao生命周期与JDBC事务联系起来?什么是可能的替代品?使UserDao单身?如果所有数据库交互都是通过单个对象完成的,那么这不会成为应用程序的瓶颈。
或者,您能否建议另一种更容易模拟的数据访问模式?
请帮忙。这是我一生中最恐怖的设计。
提前致谢。
答案 0 :(得分:2)
使用适当的模拟框架,如PowerMock或EasyMock(或两者)或Mockito,并为类实现进行具体的单元测试。不要创建UserDaoMock
类实现,因为那时你没有覆盖UseDaoImpl
类中的代码,这是单元测试的优势之一。
答案 1 :(得分:1)
您可以对gist中的LoginAction
类进行单元测试,如以下测试所示,该测试使用JMockit模拟库:
public class LoginTest
{
@Tested LoginAction action;
@Mocked ActionMapping mapping;
@Mocked HttpServletRequest request;
@Mocked HttpServletResponse response;
@Capturing UserDao userDao;
@Test
public void loginUser()
{
final String username = "user";
final String password = "password";
LoginForm form = new LoginForm();
form.setUsername(username);
form.setPassword(password);
final ActionForward afterLogin = new ActionForward("home");
new Expectations() {{
userDao.checkCredentials(username, password); result = true;
mapping.findForward("successful-login"); result = afterLogin;
}};
ActionForward forwardTo = action.execute(mapping, form, request, response);
assertSame(forwardTo, afterLogin);
new Verifications() {{ userDao.terminate(true); }};
}
}
答案 2 :(得分:1)
您需要的是the simplest thing that could possibly work。这是一个Hello World示例。
/* DAO with a data access method */
public class HelloWorldDAO
{
public String findGreeting()
{
return "Hello from the database!";
}
}
/* Struts Action class with execute method */
public class HelloWorldAction implements Action
{
private String greeting;
/* Uses indirection for DAO construction */
@Override
public String execute() throws Exception {
HelloWorldDAO dao = newHelloWorldDAO();
setGreeting(dao.findGreeting());
return SUCCESS;
}
/* The simplest possible dependency injection */
protected HelloWorldDAO newHelloWorldDAO() {
return new HelloWorldDAO();
}
public String getGreeting() {
return this.greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}
/* Unit tests for HelloWorldAction */
public class HelloWorldActionTest
{
@Test
public void testExecute() throws Exception
{
final String expectedGreeting = "Hello Test!";
String expectedForward = "success";
HelloWorldAction testAction = new HelloWorldAction() {
/* Override dependency injection method to substitute a mock impl */
@Override
protected HelloWorldDAO newHelloWorldDAO()
{
return new HelloWorldDAO() {
@Override
public String findGreeting()
{
return expectedGreeting;
}
};
}
};
String actualForward = testAction.execute();
String actualGreeting = testAction.getGreeting();
assertEquals("forward", expectedForward, actualForward);
assertEquals("greeting", expectedGreeting, actualGreeting);
}
}