如何避免课堂自用

时间:2015-07-16 14:08:23

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

我有以下课程:

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语句外,不推荐使用此解决方案的事实表明存在代码异味,确实需要重构来改进此代码。

如何删除此自用?是否有可以应用的设计模式或重构技术?

4 个答案:

答案 0 :(得分:2)

类自用并不一定是个问题:我还不确定“它应该只测试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的一般合同涉及多次调用{ {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*/
   }
}

现在这个解决方案可以自行使用覆盖方法