我有以下课程:
public class MyClass
{
public void deleteOrganization(Organization organization)
{
/*Delete organization*/
/*Delete related users*/
for (User user : organization.getUsers()) {
deleteUser(user);
}
}
public void deleteUser(User user)
{
/*Delete user logic*/
}
}
此类表示自我使用,因为其公共方法deleteOrganization
使用其他公共方法deleteUser
。在我的例子中,这个类是遗留代码,我开始添加单元测试。所以我开始在第一个方法deleteOrganization
上添加一个单元测试,最后发现这个测试已经扩展到测试deleteUser
方法。
问题是此测试不再是孤立的(它应该只测试deleteOrganization
方法)。我不得不处理与deleteUser
方法相关的不同条件,以便通过测试,这大大增加了测试的复杂性。
解决方案是监视正在测试的类和存根deleteUser
方法:
@Test
public void shouldDeleteOrganization()
{
MyClass spy = spy(new MyClass());
// avoid invoking the method
doNothing().when(spy).deleteUser(any(User.class));
// invoke method under test
spy.deleteOrganization(new Organization());
}
虽然之前的解决方案解决了这个问题,但不推荐使用spy
方法的javadoc:
像往常一样,您将阅读部分模拟警告:对象 通过划分,面向编程更难以解决复杂性问题 复杂性分离为特定的SRPy对象。部分如何 嘲笑适合这种范式?好吧,它只是没有...部分模拟 通常意味着复杂性已经转移到另一种方法 在同一个对象上。在大多数情况下,这不是您想要的方式 设计你的应用程序。
deleteOrganization
方法的复杂性已移至deleteUser
方法,这是由类自用引起的。除In most cases, this is not the way you want to design your application
语句外,不推荐使用此解决方案的事实表明存在代码异味,确实需要重构来改进此代码。
如何删除此自用?是否有可以应用的设计模式或重构技术?
答案 0 :(得分:2)
类自用并不一定是个问题:我还不确定“它应该只测试deleteOrganization方法”,不论是个人风格还是团队风格。虽然将deleteUser
和deleteOrganization
保持在独立且孤立的单元中是有帮助的,但这并不总是可行或实际的 - 特别是如果这些方法相互调用或依赖于一个共同状态。测试的目的是测试“最小的可测试单元” - 它不要求方法是独立的或者可以独立测试。
根据您的需求以及您希望代码库发展的方式,您可以做出一些选择。其中两个来自你的问题,但我正在重申它们的优点。
测试黑盒子。
如果您将MyClass视为不透明的界面,您可能不会期望或要求deleteOrganization
重复调用deleteUser
,您可以想象实现会发生变化,以至于它不会所以。 (例如,未来的升级可能会有一个数据库触发器来处理级联删除,或者单个文件删除或API调用可能会对您的组织删除进行处理。)
如果您将deleteOrganization
对deleteUser
的调用视为私有方法调用,那么您将继续测试MyClass的合同而不是其实现:创建一个包含某些用户的组织,调用该方法,并检查组织是否已经消失,用户是否也是如此。它可能很冗长,但它是最正确和最灵活的测试。
如果您希望MyClass彻底改变,或者获得全新的替代实施,这可能是一个很有吸引力的选择。
将班级水平拆分为OrganizationDeleter和UserDeleter。
要使课程更“SRPy”(即更好地符合Single Responsibility Principle),正如Mockito文档所暗示的那样,您会发现deleteUser
和deleteOrganization
是独立的。通过将它们分成两个不同的类,您可以让OrganizationDeleter接受一个模拟的UserDeleter,从而消除了对部分模拟的需要。
如果您希望用户删除业务逻辑发生变化,或者您希望编写另一个UserDeleter实现,这可能是一个很有吸引力的选项。
将班级垂直拆分为MyClass和MyClassService / MyClassHelper。
如果低级别基础架构和高级别调用之间存在足够的差异,您可能希望将它们分开。 MyClass将保留deleteUser
和deleteOrganization
,执行所需的任何准备或验证步骤,然后对MyClassService中的原语进行一些调用。 deleteUser
可能是一个简单的委托,而deleteOrganization
可以在不调用其邻居的情况下调用该服务。
如果你有足够的低级别电话来保证这种额外的分离,或者如果MyClass处理高级别和低级别的问题,这可能是一个有吸引力的选择 - 特别是如果它是你之前发生过的重构。但是要小心避免使用baklava code的图案(太多薄的,可渗透的层,而不是你想要跟踪的层)。
继续使用部分模拟。
虽然这确实泄露了内部调用的实现细节,但这确实违背了Mockito警告,总的来说,部分模拟可以提供最好的实用选择。如果你有一个方法多次调用它的兄弟,那么内部方法是一个帮助器还是一个对等体可能没有明确定义,而部分模拟可以使你不必制作那个标签。
如果您的代码有截止日期,或者实验性不足以保证完整设计,这可能是一个很有吸引力的选择。
(旁注:除非MyClass是最终的,否则它是开放的子类,这是使部分模拟成为可能的一部分。如果你要记录deleteOrganization
的一般合同涉及多次调用{ {1}},那么创建一个子类或部分模拟将是完全公平的游戏。如果没有记录,那么它是一个实现细节,应该被视为这样。)
答案 1 :(得分:0)
deleteOrganization()
的合同是否表示该组织中的所有User
也会被删除?
如果确实如此,那么您必须在deleteOrganization()
中保留至少部分删除用户逻辑,因为您班级的客户可能依赖该功能。
在我进入选项之前,我还会指出每个删除方法都是public
,而类和方法都是非最终。这将允许某人扩展该类并覆盖可能危险的方法。
解决方案1 - 删除组织仍然必须删除其用户
考虑到关于压倒一切危险的评论,我们从deleteUser()
中删除实际删除用户的部分。我假设公共方法deleteUser
执行了额外的验证,以及deleteOrganization()
不需要的其他业务逻辑。
public void deleteOrganization(Organization organization)
{
/*Delete organization*/
/*Delete related users*/
for (User user : organization.getUsers()) {
privateDeleteUser(user);
}
}
private void privateDeleteUser(User user){
//actual delete logic here, without anything delete organization doesn't need
}
public void deleteUser(User user)
{
//do validation
privateDeleteUser(user);
//perform any extra business locic
}
这会重新使用执行删除的实际代码,并避免子类更改deleteUser()
的行为方式不同的危险。
解决方案2 - 不需要删除deleteOrganization()方法中的用户
如果我们不需要从组织中删除用户,我们可以从deleteOrganization()中删除该部分代码。在我们需要删除用户的少数几个地方,我们现在可以像deleteOrganization那样进行循环。由于它使用公共方法,因此任何客户端都可以调用它们我们也可以将逻辑拉入Service
类。
public void DeleteOrganizationService(){
private MyClass myClass;
...
public void delete(Organization organization)
{
myClass.deleteOrganization(organization);
/*Delete related users*/
for (User user : organization.getUsers()) {
myClass.deleteUser(user);
}
}
}
这是更新的MyClass
public void deleteOrganization(Organization organization)
{
/*Delete organization only does not modify users*/
}
public void deleteUser(User user)
{
/*same as before*/
}
答案 2 :(得分:0)
不要监视被测试的课程,扩展它并覆盖deleteUser以做一些良性的事情 - 或者至少是在你的测试控制之下。
答案 3 :(得分:0)
Self Use of only override-able method is not proper design pattern.
Solution to above problem
You can eliminate a class’s self-use of overridable methods mechanically, without changing its behavior. Move the body of each overridable method to a private “helper method” and have each overridable method invoke its private helper method.
public class MyClass
{
public void deleteOrganization(Organization organization)
{
/*Delete organization*/
/*Delete related users*/
for (User user : organization.getUsers()) {
deleteUserHelper(user);
}
}
public void deleteUser(User user)
{
deleteUserHelper(user);
}
private void deleteUserHelper(User user){
/*delete user*/
}
}
现在这个解决方案可以自行使用覆盖方法