【问题标题】:How to avoid class self-use如何避免课堂自用
【发布时间】:2015-07-16 14:08:22
【问题描述】:

我有以下课程:

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语句,说明代码有异味,确实需要重构来改进这个代码。

这个自用怎么去掉?是否有可以应用的设计模式或重构技术?

【问题讨论】:

  • @ahmehri 与您的问题没有直接关系,但您为什么要先删除组织,然后再删除用户?不应该反过来吗?删除所有用户,然后删除组织。
  • @CKing 这是一个以这种方式实现的遗留代码(与使用的业务逻辑有关)。

标签: java unit-testing design-patterns mocking mockito


【解决方案1】:

类自用不一定是问题:我还不相信“它应该只测试 deleteOrganization 方法”,除了个人或团队风格之外的任何原因。尽管将deleteUserdeleteOrganization 保持在独立和隔离的单元中很有帮助,但这并不总是可行或实用的——尤其是当方法相互调用或依赖于一个公共状态时。测试的重点是测试“最小的可测试单元”——它不要求方法是独立的,也不需要它们可以独立测试。

您有几个选择,具体取决于您的需求以及您期望代码库如何发展。其中两个来自您的问题,但我在下面重申它们的优点。

  • 测试黑盒。

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

    如果您将 deleteOrganizationdeleteUser 的调用视为私有方法调用,那么您将只测试 MyClass 的合约而不是其实现:创建一个包含一些用户的组织,调用该方法,并检查组织消失了,用户也消失了。它可能很冗长,但它是最正确和最灵活的测试。

    如果您希望 MyClass 发生巨大变化或获得全新的替代实现,这可能是一个有吸引力的选择。

  • 将类水平拆分为 OrganizationDeleter 和 UserDeleter。

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

    如果您希望删除用户的业务逻辑有所不同,或者您希望编写另一个 UserDeleter 实现,这可能是一个有吸引力的选择。

  • 将类垂直拆分为 MyClass 和 MyClassService/MyClassHelper。

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

    如果您有足够多的低级调用来保证这种额外的分离,或者如果 MyClass 处理高级和低级关注点,这可能是一个有吸引力的选择 - 特别是如果它是您之前想到的重构。不过要小心避免baklava code 的模式(太多薄的、可渗透的层,你无法跟踪)。

  • 继续使用部分模拟。

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

    如果您的代码有过期日期,或者是实验性的不足以保证完整的设计,这可能是一个有吸引力的选择。

(旁注:确实,除非 MyClass 是最终的,否则它对子类化是开放的,这是使部分模拟成为可能的一部分。如果您要记录 deleteOrganization 的一般合同涉及对 deleteUser 的多次调用,那么创建子类或部分模拟将是完全公平的游戏。如果没有记录,那么它是一个实现细节,应该这样对待。)

【讨论】:

  • 在测试完全独立的东西 (deleteOrganization) 时,我不得不处理通过 deleteUser 方法所需的不同条件,这表明我的代码很难测试。这通常表明它设计得不好。而且我认为这与个人或团队风格无关。
  • 有些方法很难测试,特别是如果它们对给定输入的结构有很高的标准。我在这里猜测,但是如果创建一个有效用户需要大量工作,那么创建一个有效的 5 用户组织进行黑盒测试可能需要 6 倍的时间。这建议创建一个测试库或辅助方法,而不是必要重构被测系统。
  • (FWIW 你正在测试的类可能需要重构,特别是如果它混合了高级业务逻辑和低级删除操作。我自己还不能得出结论来自您迄今为止分享的内容,包括后续评论。)
【解决方案2】:

deleteOrganization() 的合同是否规定该组织中的所有 Users 也将被删除?
如果是这样,那么您必须在 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*/
}

【讨论】:

    【解决方案3】:

    不要窥探被测类,扩展它并覆盖 deleteUser 来做一些良性的事情——或者至少是在你的测试控制下的事情。

    【讨论】:

    • 这真的和部分模拟有什么不同吗?还有,间谍怎么不受测试的控制?
    【解决方案4】:
    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*/
       }
    }
    

    现在这个解决方案克服了自己使用可覆盖的方法

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-11-30
      • 1970-01-01
      • 2019-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-18
      • 1970-01-01
      相关资源
      最近更新 更多