我有以下类(class):
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*/
}
}
此类表示一种自用,因为其公共(public)方法
deleteOrganization
使用其其他公共(public)方法deleteUser
。在我的情况下,该类是旧代码,我开始在上面添加单元测试。因此,我首先针对第一种方法deleteOrganization
添加了一个单元测试,最后确定该测试已扩展为也可以测试deleteUser
方法。问题
问题在于该测试不再孤立(它应该仅测试
deleteOrganization
方法)。为了通过它,我不得不处理与deleteUser
方法相关的不同条件,以便通过测试,这极大地增加了测试的复杂性。解决方案
解决方案是监视被测类和 stub
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指出:deleteOrganization
方法的复杂性已移至deleteUser
方法,这是由类的自用引起的。除了In most cases, this is not the way you want to design your application
语句之外,不建议使用此解决方案的事实表明存在代码异味,确实需要重构来改进此代码。如何消除这种自用?是否有可以应用的设计模式或重构技术?
最佳答案
类的自用使用并不一定是问题:我尚未确信“除其他个人或团队风格外,它仅应测试deleteOrganization方法”。尽管将deleteUser
和deleteOrganization
保持在独立的隔离单元中很有帮助,但这并不总是可行或实际的-特别是如果方法彼此调用或依赖于一个公共(public)状态。测试的重点是测试“最小的可测试单元”,它不需要独立的方法或可以独立测试的方法。
您可以根据自己的需要以及代码库的发展方式进行选择。其中两个来自您的问题,但我在下面重新介绍它们的优点。
如果将MyClass视为不透明的接口(interface),则可能不会期望或要求
deleteOrganization
重复调用deleteUser
,并且可以想象实现会发生变化,而这样做不会。 (例如,将来的升级可能会使数据库触发器负责级联删除,或者单个文件删除或API调用可能会负责组织删除。)如果您将对
deleteOrganization
的deleteUser
调用视为私有(private)方法调用,那么您将只测试MyClass的契约(Contract)而不是其实现:创建具有某些用户的组织,调用该方法,并检查该组织是否消失以及用户太。它可能很冗长,但却是最正确,最灵活的测试。如果您希望MyClass发生巨大变化或获得全新的替代实现,那么这可能是一个有吸引力的选择。
正如Mockito文档所暗示的那样,为了使类更“SRPy”(即更好地符合Single Responsibility Principle),您会看到
deleteUser
和deleteOrganization
是独立的。通过将它们分成两个不同的类,可以使OrganizationDeleter接受模拟UserDeleter,从而消除了对部分模拟的需求。如果您希望用户删除业务逻辑发生变化,或者希望编写另一个UserDeleter实现,那么这可能是一个有吸引力的选择。
如果底层基础结构和高层调用之间有足够的差异,则可能需要将它们分开。 MyClass将保留
deleteUser
和deleteOrganization
,执行所需的任何准备或验证步骤,然后对MyClassService中的原语进行一些调用。 deleteUser
可能是一个简单的委托(delegate),而deleteOrganization
可以在不调用其邻居的情况下调用该服务。如果您有足够的低级调用来保证这种额外的分离,或者MyClass处理高级和低级问题,这可能是一个有吸引力的选择,尤其是在您之前曾经进行过重构的情况下。但是,请小心避免使用baklava code模式(比您所能跟踪的层薄得多的可渗透层)。
尽管这确实泄漏了内部调用的实现细节,并且确实违反了Mockito警告,但总的来说,部分模拟可以提供最佳的实用选择。如果您有一个方法多次调用其同级对象,那么内部方法是助手还是对等对象可能无法很好地定义,并且部分模拟可以使您不必制作该标签。
如果您的代码有有效期限,或者具有足够的实验性,不能保证完整的设计,那么这可能是一个有吸引力的选择。
(旁注:的确,除非MyClass是最终版本,否则它可以进行子类化,这是部分模拟成为可能的一部分。如果要记录
deleteOrganization
的总契约(Contract)涉及对deleteUser
的多次调用,那么这将是完全公平的创建子类或部分模拟的游戏。如果未记录,则为实现细节,应将其视为此类。)