单元测试、集成测试还是设计中的问题?
我编写了我的第一个单元测试,我认为它过于依赖其他模块,我不确定是否是因为:
- 这是一个复杂的测试,
- 我实际上编写了集成测试,或者
- 我的设计有问题
我首先要说的是,虽然我有大约 4 年的开发经验,但我从未学过、也没有人教过自动化测试。
我刚刚使用 Hibernate 完成了 DAL 实现的重大更改,我的一位同事建议我为新部分编写单元测试。
主要变化是切换到“每个请求会话”模式以及更有建设性地使用应用程序事务。
由于上述更改的性质,单元测试从特定请求到达并开始事务的点开始,测试在事务结束后结束,并检查事务是否执行了应有的更改。
这一测试涉及初始化以下对象:
- 内存数据库 - 以便有数据可供使用。
- 初始化公司记录器 - 方法取决于它。
- 初始化一个设计为单例的存储库 - 它是函数通往 DAL 的大门,但它也存储其他内容,因此它是一个需要创建的大对象。
- 初始化请求处理程序,它也是一个单例——它包含要测试的方法。
我想我实际上已经编写了一个集成测试,因为我需要初始化数据库、Hibernate 和存储库,但我不确定如何编写它,否则考虑到测试方法使用所有这些对象的情况它的操作,我有兴趣了解事务处理如何执行(这是在测试方法上完成的)。
我很感激所有的评论和想法,如果不够清楚,我会很乐意详细说明或澄清。
谢谢,
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();
}
}
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.
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.
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.
- 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.
- 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).
I'd appreciate all comments and thoughts and will gladly elaborate or clear things up if they are not clear enough.
Thanks,
Ittai
P.S. The HibernateSessionFactory
is in-fact the commonly known HibernateUtil
from the Hibernate In Action
book, wrongly named for historical reasons.
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();
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
是的,这绝对是一个集成测试。集成测试没有任何问题,它们是测试策略的重要组成部分,但它们必须仅限于验证模块是否正确组装以及配置是否正确设置。
如果你开始使用它们来测试功能,你会得到太多的它们,并且会发生 2 件非常糟糕的事情:
测试变得非常慢
过早设计僵化集< /p>
后一个问题是因为您现在在集成测试中耦合您的设计,即使模块本身是完美解耦的。如果你找到重构的机会,很可能会破坏十几个集成测试,要么你没有勇气,要么管理层会阻止你清理(“我工作!!!不要碰它”综合症) )。
解决方案是通过“模拟”环境来对您编写的所有部分进行单元测试。有一些很好的框架可以帮助动态创建模拟对象,我个人经常使用 EasyMock。然后,您描述与世界其他地方的交互,同时验证方法的功能。
在单元测试中,您现在将获得代码所依赖的依赖项的详细描述。您还将在这里发现设计问题,因为如果您在单元测试中得到复杂的模拟行为,则意味着存在设计问题。这是很好的早期反馈。
对基础设施代码进行单元测试是没有意义的,因为它可能已经经过单元测试,而且无论如何您都无能为力。
然后添加 1 或 2 个有针对性的集成测试,以验证所有部件按预期工作、查询返回正确的对象、事务得到正确处理等...
这平衡了验证组装时一切正常的需要,并能够重构,缩短测试时间并设计松散耦合的模块。
但要实现这一目标确实需要一些经验。我建议找一位以前做过这件事的经验丰富的开发人员,并提供饮料以换取该领域的指导。问很多问题。
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 :
The tests become frustratingly slow
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. 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...
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.