【问题标题】:Unit-Test, Integration test or problem in design?单元测试、集成测试或设计问题?
【发布时间】:2010-10-03 09:38:17
【问题描述】:

我写了我的第一个单元测试,我认为它过于依赖其他模块,我不确定是不是因为:

  • 这是一个复杂的测试
  • 我实际上已经编写了一个集成测试或
  • 我的设计有问题

我首先要说的是,虽然我有大约 4 年的开发经验,但我从来没有学过,也没有教过自动化测试。
我刚刚使用 Hibernate 完成了 DAL 实现的重大更改,我的一位同事建议我为新部分编写单元测试。
主要变化在于切换到每个请求会话模式和更具建设性地使用应用程序事务。
由于上述更改的性质,单元测试从特定请求到达并开始事务的点开始,并且测试在事务结束后结束,并检查事务是否执行了它应该执行的更改。
这一测试涉及初始化以下对象:

  • 内存中的 DB- 以便有数据可以处理。
  • 初始化公司记录器 - 因为方法取决于它。
  • 初始化设计为单例的存储库 - 它是 DAL 的函数入口,但它也存储其他内容,因此它是一个需要创建的大对象。
  • 初始化同样是单例的请求处理程序 - 这包含要测试的方法。

我想我实际上已经编写了一个集成测试,因为我需要初始化数据库、Hibernate 和存储库,但我不确定我该如何编写它,否则考虑到测试方法使用所有的情况这些对象的作用,我很想看看事务处理是如何执行的(这是在测试方法上完成的)。

我会很感激所有的 cmets 和想法,如果他们不够清楚,我会很乐意详细说明或澄清问题。

谢谢,
伊泰

附: HibernateSessionFactory 实际上是 Hibernate In Action 书中众所周知的 HibernateUtil,由于历史原因而被错误地命名。

public class AdminMessageRepositoryUpdaterTest {
private static WardId wardId;
private static EmployeeId employeeId;
private static WardId prevWardId;
private static EmployeeId prevEmployeeId;

@Test
public void testHandleEmployeeLoginToWard(){
    AgentEmployeesWardsEngine agentEmployeesWardsEngine = new AgentEmployeesWardsEngine();
    AgentEngine agentEngine = new AgentEngine();
    //Remove all entries from AgentEmployeesWards table
    HibernateSessionFactory.beginTransaction();
    for (Agent agent : agentEngine.findAll()){
        agentEmployeesWardsEngine.removeAgentEntries(agent.getId());
    }
    HibernateSessionFactory.commitTransaction();//no need to try catch as this is done in a controlled environment
    int i=0;
    //build expectedSet
    Set<AgentEmployeesWards> expectedMappingsToChangeSet = new HashSet<AgentEmployeesWards>();
    //Mappings which should have ward updated
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(1).getValue(), employeeId.getValue(), prevWardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(2).getValue(), employeeId.getValue(), prevWardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    //Mappings which should have employee updated
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(3).getValue(), prevEmployeeId .getValue(), wardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(4).getValue(), prevEmployeeId.getValue(), wardId.getValue(), false, TimestampUtils.getTimestamp(), i++));

    //Prepare clean data for persistence
    Set<AgentEmployeesWards> cleanSet = new HashSet<AgentEmployeesWards>(expectedMappingsToChangeSet);
    //Mappings which should NOT have ward updated
    cleanSet.add(new AgentEmployeesWards(new AgentId(5).getValue(), employeeId.getValue(), prevWardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    cleanSet.add(new AgentEmployeesWards(new AgentId(6).getValue(), employeeId.getValue(), prevWardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    //Mappings which should NOT have employee updated
    cleanSet.add(new AgentEmployeesWards(new AgentId(7).getValue(), prevEmployeeId .getValue(), wardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    cleanSet.add(new AgentEmployeesWards(new AgentId(8).getValue(), prevEmployeeId.getValue(), wardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    HibernateSessionFactory.beginTransaction();
    for (AgentEmployeesWards agentEmployeesWards : cleanSet){
        agentEmployeesWardsEngine.saveNewAgentEmployeesWardsEntry(agentEmployeesWards);
    }
    HibernateSessionFactory.commitTransaction();//no need to try catch as this is done in a controlled environment
    //Close the session as to neutralize first-level-cache issues
    HibernateSessionFactory.closeSession();
    //Perform the action so it can be tested
    AdminMessageReposityUpdater.getInstance().handleEmployeeLoginToWard(employeeId, wardId, TimestampUtils.getTimestamp());

    //Close the session as to neutralize first-level-cache issues
    HibernateSessionFactory.closeSession();

    //Load actualSet from DAL
    Set<AgentEmployeesWards> actualSet = new HashSet<AgentEmployeesWards>(agentEmployeesWardsEngine.findByPrimaryEmployeeId(employeeId));
    actualSet.addAll(agentEmployeesWardsEngine.findByPrimaryWardId(wardId));

    //Prepare expected
    Set<AgentEmployeesWards> expectedSet = new HashSet<AgentEmployeesWards>();
    for (AgentEmployeesWards agentEmployeesWards : expectedMappingsToChangeSet){
        //We need to copy as the wardId and employeeId are properties which comprise the equals method of the class and so 
        //they cannot be changed while in a Set
        AgentEmployeesWards agentEmployeesWardsCopy = new AgentEmployeesWards(agentEmployeesWards);
        if (agentEmployeesWardsCopy.isEmployeePrimary()){
            //If this is a employee primary we want it to be updated to the new org-unit id
            agentEmployeesWardsCopy.setWardId(wardId.getValue());
        } else {
            //Otherwise we want it to be updated to the new employee id
            agentEmployeesWardsCopy.setEmployeeId(employeeId.getValue());
        }
        expectedSet.add(agentEmployeesWardsCopy);
    }
     //Assert between actualSet and expectedSet
    // Assert actual database table match expected table
   assertEquals(expectedSet, actualSet);


}
@BeforeClass
public static void setUpBeforeClass() throws SQLException,ClassNotFoundException{
    Class.forName("org.h2.Driver");
    Connection conn = DriverManager.getConnection("jdbc:h2:mem:MyCompany", "sa", "");

    ConfigurationDAO configDAO = new ConfigurationDAO();
    HibernateSessionFactory.beginTransaction();
    configDAO.attachDirty(new Configuration("All","Log", "Level", "Info",null));
    configDAO.attachDirty(new Configuration("All","Log", "console", "True",null));
    configDAO.attachDirty(new Configuration("All","Log", "File", "False",null));

    HibernateSessionFactory.commitTransaction();
    Logger log = new Logger();
    Server.getInstance().initialize(log);
    Repository.getInstance().initialize(log);
    AdminMessageReposityUpdater.getInstance().initialize(log);

    AdminEngine adminEngine = new AdminEngine();
    EmployeeEngine employeeEngine = new EmployeeEngine();
    HibernateSessionFactory.beginTransaction();
    Ward testWard = new Ward("testWard", 1, "Sales", -1, null);
    adminEngine.addWard(testWard);
    wardId = new WardId(testWard.getId());
    Ward prevWard = new Ward("prevWard", 1, "Finance", -1, null);
    adminEngine.addWard(prevWard);
    prevWardId = new WardId(prevWard.getId());

    Employee testEmployee = new Employee("testEmployee", "test", null, "employee", "f", prevWardId.getValue(), null, false, true);
    employeeEngine.setEmployee(testEmployee);
    employeeId = new EmployeeId(testEmployee.getId());

    Employee prevEmployee = new Employee("prevEmployee", "prev", null, "employee", "f", wardId.getValue(), null, false, true);
    employeeEngine.setEmployee(prevEmployee);
    prevEmployeeId = new EmployeeId(prevEmployee.getId());
    HibernateSessionFactory.commitTransaction();
    HibernateSessionFactory.closeSession();
}
@AfterClass
public static void tearDownAfterClass(){
    AdminEngine adminEngine = new AdminEngine();
    EmployeeEngine employeeEngine = new EmployeeEngine();
    HibernateSessionFactory.beginTransaction();
    employeeEngine.removeEmployeeById(employeeId);
    employeeEngine.removeEmployeeById(prevEmployeeId);
    adminEngine.removeWardById(wardId);
    adminEngine.removeWardById(prevWardId);
    HibernateSessionFactory.commitTransaction();
    HibernateSessionFactory.closeSession();
}
}

【问题讨论】:

    标签: java unit-testing hibernate integration-testing


    【解决方案1】:

    是的,这绝对是一个集成测试。集成测试没有错,它们是测试策略的重要组成部分,但它们必须仅限于验证模块是否正确组装和配置是否正确设置。

    如果你开始使用它们来测试功能,你会得到太多它们并且会发生 2 件非常糟糕的事情:

    1. 测试变得慢得令人沮丧

    2. 设计僵化过早

    后一个问题是因为您现在在集成测试中耦合了您的设计,即使模块本身是完全解耦的。如果你找到一个重构的机会,它很可能会破坏十几个集成测试,或者你找不到勇气,或者管理层会阻止你清理(“我工作!!!不要碰它”综合症)。

    解决方案是通过“模拟”环境来对您编写的所有部分进行单元测试。有很好的框架可以帮助动态地制作模拟对象,我个人经常使用 EasyMock。然后,您在验证方法的功能时描述与世界其他地方的交互

    在单元测试中,您现在将获得关于您的代码所依赖的依赖项的详细描述。您还会在这里发现设计问题,因为如果您在单元测试中得到令人费解的模拟行为,则意味着存在设计问题。这是很好的早期反馈。

    对基础架构代码进行单元测试是没有意义的,因为它可能已经过单元测试,而且无论如何您都无能为力。

    然后添加 1 或 2 个有针对性的集成测试,以验证所有部分是否按预期工作、查询返回正确的对象、事务是否得到正确处理等等......

    这平衡了在组装时验证一切是否正常的需求,具有重构能力、缩短测试时间和设计松散耦合模块的能力。

    虽然要完成它确实需要一些经验。我建议找一个有经验的开发人员,他以前做过这方面的工作,并提供饮料以换取这方面的指导。问很多问题。

    【讨论】:

    • 非常感谢您的评论。你的意思是我应该编写一个隔离测试,它只测试我对 DAL 实现所做的更改,即每个请求的会话和事务,然后假设每个使用 DAL 的地方都可以正常工作我的实现本身是否经过测试?
    • 哦,还有一件事,如果我有一个只改变外部对象状态的方法,当然是通过它自己的逻辑。您将如何以及是否会对其进行单元测试?如果更清楚,我可以更新问题以显示这种方法的示例
    • 总之是的。如果您的基础架构代码适用于用例 A 并且已正确解耦,那么它也适用于用例 B、C、D,也许您需要对用例 E 进行另一个测试。您还可以查看 Unitils:unitils.org/tutorial.html 它提供了很好的支持使用测试数据集并简化集成测试。只是不要过度。
    • 对于更新:模拟 Session、Query 对象,设置预期调用并返回所需对象,并验证使用正确更新的对象调用更新方法。
    • 非常感谢,我想我需要在整个Mocking技术上做更多的学习,但我想我明白了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多