爪哇。模拟数据访问对象

Posted

技术标签:

【中文标题】爪哇。模拟数据访问对象【英文标题】:Java. Mock data access object 【发布时间】:2015-02-08 20:30:29 【问题描述】:

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

我正在使用 JavaEE、Struts 和我的自定义数据访问逻辑开发 Web 应用程序。在这个应用程序中,Struts Action 使用 UserDao 来检索用户对象。 UserDao 对象的生命周期与 JDBC 事务相关联。想法是,当 Action 创建 UserDao 对象时,它启动 JDBC 事务(必要的 JDBC 内容存储在 UserDao 对象中),所有 UserDao 方法的调用都在单个 JDBC 事务中运行,然后 Action 终止 UserDao 对象完成事务(提交或回滚)。

问题在于,在操作测试期间,我需要模拟此 UserDao 以使其返回带有必要测试数据的用户对象。

到目前为止,我发现的唯一解决方案是可怕的。我做了以下工作:将 UserDao 拆分为 UserDao 接口和实现它的 UseDaoImpl 类。该接口也将由返回必要的测试数据的 UserDaoMock 实现。接下来,我需要一些在生产运行中返回真实 UserDao (UserDaoImpl) 并在测试运行中模拟 (UserDaoMock) 的结构。此结构不应依赖于 UserDaoMock 类型,以使应用程序的生产代码独立于模拟 .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 单例?如果所有数据库交互都通过单个对象完成,这不会成为应用程序瓶颈吗?

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

请帮忙。这是我一生中做过的最糟糕的设计。

提前致谢。

【问题讨论】:

【参考方案1】:

使用适当的模拟框架,如 PowerMock 或 EasyMock(或两者)或 Mockito,并对类实现进行具体的单元测试。不要创建UserDaoMock 类实现,因为这样您就没有覆盖UseDaoImpl 类中的代码,这是单元测试的优势之一。

【讨论】:

哦,对不起。那还不够清楚。被测代码是我的 Struts Action,这就是我需要模拟 UserDao 的原因。 然后使用适当的模拟框架模拟您的界面,使其返回必要的数据。 我的问题不在于通过接口生成模拟,而是在真实对象和模拟之间进行调度。 Fabric 和提取的接口应该允许对 Action 透明,无论它接收真正的 UserDaoImpl 还是 UserDaoMock,但我无法实现这样的设计。主要问题是我需要结构中对象的“示例”才能分配另一个对象。如果 UserDao 是单例的,这将解决问题,但在这种情况下,与数据库的交互将成为应用程序的瓶颈。 @yaromir 在使用适当的模拟框架时(如现在所说的 3 次),您根本不需要使用这种结构。您只需几行代码即可创建UserDaoImpl 的实例,其中的方法返回准备好的数据以进行测试。 @yaromir 还有,你的UserDaoImpl不得(是的,记住这一点)是单身人士。【参考方案2】:

您需要的是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);
    

【讨论】:

模拟HelloWorldDAO 的解决方案更简单,因为它不需要被测类中的newHelloWorldDAO 方法。此外,它将允许 HelloWorldAction 成为 final 类。 非常感谢!!!正是我一直在寻找的。 @Rogério Mock 可以通过实现与真实对象相同的接口或通过从真实对象继承来创建。如果您选择子类化方式,则无论如何都必须允许继承。 @yaromir 我所说的模拟是完全通用的:它同样(使用相同的模拟 API)适用于接口、抽象类、具体类、最终类、枚举等。模拟一个类,不需要创建子类,因为这只是一种特殊的技术。 @Rogério 您的观点是合理的,尽管当一个完整清晰的工作示例需要如此少的代码行且没有额外的依赖项时,很难质疑此解决方案的简单性。 @gknicker 好吧,我描述的解决方案需要 less 行代码。它更简单,根据article第二页第一段中描述的简单定义。【参考方案3】:

您可以对 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); ;
    

【讨论】:

以上是关于爪哇。模拟数据访问对象的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript笔记--- JSON(对象的创建,访问对象属性等;eval函数;模拟将数据库中的信息打印在页面的表格中)

模拟复杂的对象 - 用Moq测试

浅谈面向对象之封装继承多态!如何使用内部类模拟多继承

模拟在内存中的数据库DataSet相关的类

Java堆内存溢出模拟

多次使用相同的模拟对象/数组时,如何在没有引用问题的情况下导入模拟数据以进行测试