Java的。模拟数据访问对象

时间:2014-12-10 15:16:04

标签: java unit-testing jdbc mocking dao

我遇到了模拟数据访问逻辑的问题。

我正在使用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方法。)

    公共类UserDaoFabric {

    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单身?如果所有数据库交互都是通过单个对象完成的,那么这不会成为应用程序的瓶颈。

或者,您能否建议另一种更容易模拟的数据访问模式?

请帮忙。这是我一生中最恐怖的设计。

提前致谢。

3 个答案:

答案 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);
    }
}