我有以下类(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方法”。尽管将deleteUserdeleteOrganization保持在独立的隔离单元中很有帮助,但这并不总是可行或实际的-特别是如果方法彼此调用或依赖于一个公共(public)状态。测试的重点是测试“最小的可测试单元”,它不需要独立的方法或可以独立测试的方法。

您可以根据自己的需要以及代码库的发展方式进行选择。其中两个来自您的问题,但我在下面重新介绍它们的优点。

  • 测试黑盒。

    如果将MyClass视为不透明的接口(interface),则可能不会期望或要求deleteOrganization重复调用deleteUser,并且可以想象实现会发生变化,而这样做不会。 (例如,将来的升级可能会使数据库触发器负责级联删除,或者单个文件删除或API调用可能会负责组织删除。)

    如果您将对deleteOrganizationdeleteUser调用视为私有(private)方法调用,那么您将只测试MyClass的契约(Contract)而不是其实现:创建具有某些用户的组织,调用该方法,并检查该组织是否消失以及用户太。它可能很冗长,但却是最正确,最灵活的测试。

    如果您希望MyClass发生巨大变化或获得全新的替代实现,那么这可能是一个有吸引力的选择。
  • 将类水平拆分为OrganizationDeleter和UserDeleter。

    正如Mockito文档所暗示的那样,为了使类更“SRPy”(即更好地符合Single Responsibility Principle),您会看到deleteUserdeleteOrganization是独立的。通过将它们分成两个不同的类,可以使OrganizationDeleter接受模拟UserDeleter,从而消除了对部分模拟的需求。

    如果您希望用户删除业务逻辑发生变化,或者希望编写另一个UserDeleter实现,那么这可能是一个有吸引力的选择。
  • 将类垂直拆分为MyClass和MyClassService/MyClassHelper。

    如果底层基础结构和高层调用之间有足够的差异,则可能需要将它们分开。 MyClass将保留deleteUserdeleteOrganization,执行所需的任何准备或验证步骤,然后对MyClassService中的原语进行一些调用。 deleteUser可能是一个简单的委托(delegate),而deleteOrganization可以在不调用其邻居的情况下调用该服务。

    如果您有足够的低级调用来保证这种额外的分离,或者MyClass处理高级和低级问题,这可能是一个有吸引力的选择,尤其是在您之前曾经进行过重构的情况下。但是,请小心避免使用baklava code模式(比您所能跟踪的层薄得多的可渗透层)。
  • 继续使用部分模拟。

    尽管这确实泄漏了内部调用的实现细节,并且确实违反了Mockito警告,但总的来说,部分模拟可以提供最佳的实用选择。如果您有一个方法多次调用其同级对象,那么内部方法是助手还是对等对象可能无法很好地定义,并且部分模拟可以使您不必制作该标签。

    如果您的代码有有效期限,或者具有足够的实验性,不能保证完整的设计,那么这可能是一个有吸引力的选择。

  • (旁注:的确,除非MyClass是最终版本,否则它可以进行子类化,这是部分模拟成为可能的一部分。如果要记录deleteOrganization的总契约(Contract)涉及对deleteUser的多次调用,那么这将是完全公平的创建子类或部分模拟的游戏。如果未记录,则为实现细节,应将其视为此类。)

    10-06 10:17