简体   繁体   English

单元测试,集成测试还是设计中的问题?

[英]Unit-Test, Integration test or problem in design?

I'm written my first unit-test and I think it is too dependent on other modules and I'm not sure whether it's because: 我写了我的第一个单元测试,我认为它太依赖于其他模块了,我不确定是否是因为:

  • It's a complex test 这是一个复杂的测试
  • I've actually written an integration test or 我实际上已经编写了集成测试或
  • I have a problem in my design 我的设计有问题

I'll first say that although I have around 4 years of experience in development I never learned, nor were taught, automated testing. 我首先要说的是,尽管我有大约4年的开发经验,但我从未学习过自动测试,也从未教过它。
I've just finished a major change in our DAL implementation, with Hibernate, and a colleague of mine suggested I write unit-tests for the new parts. 我刚刚与Hibernate一起完成了DAL实现的一项重大更改,我的一位同事建议我为新零件编写单元测试。
The main change was with respect to switching to the Session-per-Request pattern and the more constructive use of application transactions. 主要变化在于切换到“按请求会话”模式和对应用程序事务的更具建设性的使用。
Because of the nature of the above change the unit-test begins at the point where a specific request arrives and begins a transaction and the test ends after the transaction ends and it checks whether the transaction performed the changes it was supposed to. 由于上述更改的性质,单元测试从特定请求到达并开始事务的点开始,并且测试在事务结束后结束,并且它检查事务是否执行了应该执行的更改。
This one test involves initializing the following objects: 此测试涉及初始化以下对象:

  • In-memory DB- so that there will be data to work against. 内存中的DB-,以便可以处理数据。
  • Initialize company logger- as method depends on it. 初始化公司记录器-方法取决于它。
  • Initialize a repository designed as a singleton-it is the function's gate to the DAL, but it also stores other things so it's a big object to create. 初始化一个设计为单例的存储库-它是函数访问DAL的大门,但它也存储其他内容,因此是创建对象。
  • Initialize the handler of requests which is also a singleton- this holds the method to be tested. 初始化也是单例的请求处理程序-保留要测试的方法。

I think I've actually written an integration test, as I need to init the DB, Hibernate and the repository, but I'm not sure how I could've written it otherwise given the circumstances where the tested method uses all these objects for its action and I'm interested to see how the transaction handling performs (which is done on the tested method). 我想我实际上已经编写了一个集成测试,因为我需要初始化数据库,Hibernate和存储库,但是由于测试对象将所有这些对象用于所有情况,我不确定如何编写它。它的动作,我很想看看事务处理如何执行(在测试的方法上完成)。

I'd appreciate all comments and thoughts and will gladly elaborate or clear things up if they are not clear enough. 我很感谢所有评论和想法,如果它们不够清晰,将很乐意详细阐述或整理。

Thanks, 谢谢,
Ittai 以太

PS The HibernateSessionFactory is in-fact the commonly known HibernateUtil from the Hibernate In Action book, wrongly named for historical reasons. PS 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();
}
}

Yep, this definitely is an integration test. 是的,这绝对是一个集成测试。 There is nothing wrong wih integration tests and they are an important part of a test strategy, but they must be limited to verifying iif the modules are properly assembled and the configuration is properly set. 集成测试没有错,它们是测试策略的重要组成部分,但必须仅限于验证模块是否正确组装以及配置是否正确。

If you start using them to test functionality, you will get too much of them and 2 very bad things happen : 如果您开始使用它们来测试功能,则会得到太多的结果,并且会发生2件非常糟糕的事情:

  1. The tests become frustratingly slow 测试令人沮丧地缓慢

  2. Design ossification sets in too early 设计骨化设置为时过早

The latter problem is because you are now coupling your design in the integration tests, even if the modules themselves are perfectly decoupled. 后一个问题是因为您现在正在集成测试中耦合您的设计,即使模块本身已完美耦合。 If you find an opportunity to refactor, chances are it will break a dozen integration tests, and either you won't find the courage, or management will prevent you from cleaning up (The "I works!!! Do not touch it" syndrome). 如果您找到重构的机会,那么它很可能会破坏十几个集成测试,或者您将找不到勇气,或者管理层会阻止您清理工作(“我努力!不要碰它”综合症)。

The solution is to unit test all parts you have written by "mocking" out the environment. 解决方案是通过“模拟”环境来对您编写的所有部分进行单元测试。 There are nice frameworks to help make mock objects on the fly, I personally use EasyMock a lot. 有很多不错的框架可以帮助快速制作模拟对象,我个人经常使用EasyMock。 YOu then describe the interactions with the rest of the world while verifying the functionality of your methods 然后,您将描述与世界其他地方的交互,同时验证方法的功能

In the unit tests you will get now a nice detailed description of the dependencies your code is relying on. 在单元测试中,您现在将获得有关代码所依赖的依赖项的详细说明。 You will also spot design problems here because if you get convoluted mock behavior in the unit tests, then it means there are design issues. 您还会在这里发现设计问题,因为如果在单元测试中出现复杂的模拟行为,则意味着存在设计问题。 This is great early feedback. 这是很好的早期反馈。

It does not make sense to unit-test the infrastructure code, as it probably already has been unit-tested, and there is nothing you can do about it anyway. 对基础结构代码进行单元测试是没有意义的,因为它可能已经进行了单元测试,并且您对此无能为力。

Then add 1 or 2 targeted integration tests to verify all the parts work as expected, the queries return the right objects, the transactions are properly handled, etc... 然后添加1或2个针对性的集成测试,以验证所有部件是否按预期工作,查询返回正确的对象,正确处理事务等。

This balances out the need to verify everything works when assembled, with the ability to refactor, keeping your test times short and designeing loosely coupled modules. 这样就平衡了验证组装后一切正常的需求,重构的能力,缩短测试时间并设计松耦合模块的需求。

It does take some experience though to pull it off. 确实需要一些经验。 I would recommend to find an experienced developer who has done this before and offer beverages in exchange for mentoring in this area. 我建议找一个经验丰富的开发人员,他曾经做过此事,并提供饮料以换取该领域的指导。 Ask a lot of questions. 问很多问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM