我有以下类:
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
语句之外,不建议使用此解决方案这一事实表明存在代码异味,并且确实需要重构来改进此代码。
如何删除这种自用?是否有可以应用的设计模式或重构技术?
类自用不一定是问题:我还不相信"它应该只测试deleteOrganization方法",除了个人或团队风格之外的任何原因。尽管将deleteUser
和deleteOrganization
保留为独立和孤立的单元很有帮助,但这并不总是可行或实用的 - 特别是当这些方法相互调用或依赖于一个共同状态时。测试的重点是测试"最小的可测试单元",这不要求方法独立或可以独立测试。
有几种选择,具体取决于您的需求以及您期望代码库如何发展。其中两个来自您的问题,但我在下面用优点重申它们。
测试黑匣子。
如果你把MyClass看作一个不透明的接口,你可能不会期望或要求
deleteOrganization
重复调用deleteUser
,你可以想象实现的变化使得它不会这样做。(例如,将来的升级可能会让数据库触发器处理级联删除,或者单个文件删除或 API 调用可能会处理组织删除。如果您将
deleteOrganization
对deleteUser
的调用视为私有方法调用,那么您将测试MyClass的合约而不是其实现:创建一个包含一些用户的组织,调用该方法,并检查组织是否消失以及用户是否也消失了。它可能很冗长,但它是最正确和灵活的测试。如果您希望MyClass发生巨大变化,或者获得全新的替代实现,这可能是一个有吸引力的选择。
将类水平拆分为 OrganizationDeleter 和 UserDeleter。
为了使类更"SRPy"(即更好地符合单一责任原则(,正如 Mockito 文档所暗示的那样,您会看到
deleteUser
和deleteOrganization
是独立的。通过将它们分成两个不同的类,您可以让 OrganizationDeleter 接受模拟 UserDeleter,从而消除对部分模拟的需要。如果您希望用户删除业务逻辑发生变化,或者您希望编写另一个 UserDeleter 实现,则这可能是一个有吸引力的选项。
将类垂直拆分为 MyClass 和 MyClassService/MyClassHelper。
如果低级别基础结构和高级调用之间存在足够的差异,则可能需要将它们分开。MyClass将保持
deleteUser
和deleteOrganization
,执行所需的任何准备或验证步骤,然后对MyClassService中的原语进行一些调用。deleteUser
可能是一个简单的委托,而deleteOrganization
可以在不调用其邻居的情况下调用服务。如果你有足够的低级调用来保证这种额外的分离,或者如果 MyClass 同时处理高级和低级问题,这可能是一个有吸引力的选择——特别是如果它是你之前发生的重构。不过,要小心避免果仁蜜饼代码的模式(太多的薄而可渗透的层,无法跟踪(。
继续使用部分模拟。
虽然这确实泄露了内部调用的实现细节,并且这确实违背了 Mockito 警告,但总的来说,部分模拟可以提供最好的务实选择。如果一个方法多次调用其同级,则内部方法可能是帮助程序还是对等方,可能没有明确定义,并且部分模拟将使您无需创建该标签。
如果您的代码具有到期日期,或者具有足够的实验性,无法保证完整的设计,则这可能是一个有吸引力的选择。
(旁注:确实,除非MyClass是最终的,否则它可以对子类开放,这是使部分模拟成为可能的部分原因。如果你要记录deleteOrganization
的总契约涉及对deleteUser
的多次调用,那么创建一个子类或部分模拟将是完全公平的游戏。如果未记录,则它是实现详细信息,应按此处理。
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(( 中删除这部分代码。 在我们也需要删除用户的少数地方,我们可以像现在删除组织一样执行循环。 由于它使用公共方法,因此只有任何客户端都可以调用它们。 我们也可以将逻辑提取到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*/
}
不要监视被测试的类,扩展它并覆盖 deleteUser 以执行一些良性的事情——或者至少在你的测试控制下做一些事情。
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*/
}
}
现在这个解决方案克服了可覆盖方法的自我使用