像isInUnitTest()这样的检查是反模式吗?

时间:2013-07-28 01:49:36

标签: unit-testing design-patterns language-agnostic anti-patterns

我正在开发一个个人项目(意思是干净的源代码,没有遗留的依赖项),并尝试遵循有关单元测试,依赖关系管理等方面的最佳实践。

我公司的代码库中充斥着这样的代码:

public Response chargeCard(CreditCard card, Money amount) {
  if(Config.isInUnitTests()) {
      throw new IllegalStateException("Cannot make credit card API calls in unit tests!");
  }
  return CreditCardPOS.connect().charge(card, amount);
}

这里的目标是主动防止在测试期间执行具有外部依赖性的危险代码/代码。如果单元测试做坏事我喜欢快速失败的概念,但我不喜欢这个实现有几个原因:

  • 它将隐藏的依赖项留给分散在整个代码库中的静态Config类。
  • 它改变了测试和实时行为之间的控制流程,这意味着我们不一定要测试相同的代码。
  • 它将一个外部依赖项添加到配置文件或其他一些状态保持实用程序。
  • 看起来很丑:)

在我公司的代码库中使用这个的相当多的地方可以通过我试图做的更好的依赖感知来避免,但是仍然有一些地方我仍然在努力避免实施isInUnitTests()方法。

使用上面的信用卡示例,我可以避免isInUnitTests()检查每次充电,将其正确地包装在一个可模拟的CardCharger课程或类似的东西中,但我觉得更干净我只是将问题提升到一个级别 - 如何在不对创建它的构造函数/工厂方法进行检查的情况下阻止单元测试构建CardCharger的实例?

  • 代码有isInUnitTests()气味吗?
  • 如果是这样,我如何才能强制执行单元测试无法达到外部依赖关系?
  • 如果没有,实施此类方法的最佳方法是什么,以及何时使用/避免它的好方法是什么?

为了澄清,我正在尝试阻止单元测试访问不可接受的资源,如数据库或网络。我喜欢依赖注入的测试友好模式,但如果粗略的开发人员(即我)可能会违反好模式,那么好的模式就没用了 - 在单元测试做事情的情况下,对我来说快速失败似乎很重要不应该,但我不确定最好的方法。

6 个答案:

答案 0 :(得分:5)

  

isInUnitTests()有代码味吗?

是的,确切地说,您有很多方法可以避免将代码与单元测试结合起来。没有正当理由有类似的东西。

  

如果是这样,我怎么能继续强制单元测试没有达到外部依赖?

您的测试必须只验证一个代码单元并为外部依赖项创建mock or stubs

你的代码似乎是Java,它有很多成熟的模拟框架。研究一下现有的,然后选择一个更喜欢你的人。

修改

  

如何在不对创建它的构造函数/工厂方法进行检查的情况下,如何阻止单元测试构建HTTPRequest的实际实例

您应该使用依赖注入器来解决您的实例依赖关系,这样您就不需要使用if来测试您是否正在进行测试,因为您注入了“真实”代码完整的功能依赖项,并在测试中注入模拟或存根。

  

举一个更严肃的例子,比如信用卡充电器(经典的单元测试示例) - 如果测试代码甚至可以访问真正的卡充电器而不会引发大量异常,那么这似乎是一个非常糟糕的设计。我不认为在这样的情况下,相信开发人员总能做正确的事情就足够了

同样,你应该注入外部依赖关系作为信用卡充电器,所以在你的测试中你会注入一个假的。如果某些开发人员不知道这一点,我认为贵公司需要的第一件事就是为该开发人员提供培训,并使用一些编程来指导流程。

在任何情况下,我都明白你的意思,让我告诉你我遇到的类似情况。

有一个应用程序在经过一些处理之后发送了一堆电子邮件。除了直播之外,这些电子邮件不会发送到任何其他环境,否则这将是一个大问题。

在我开始处理这个应用程序之前,发生过几次开发人员“忘记”这个规则并且电子邮件是在测试环境中发送的,这导致了很多问题。依靠人类记忆来避免这种问题并不是一个好的策略。

我们为避免再次发生这种情况所做的是添加配置设置以指示是否发送真实的电子邮件。正如你所看到的,问题比在单元测试中执行或不执行更广泛。

但是没有什么可以替代通信,开发人员可以在他的开发环境中为此设置设置不正确的值。你永远不会百分百安全。

简而言之:

  • 第一道防线是沟通和培训。
  • 你的第二道防线应该是依赖注入。
  • 如果您觉得这还不够,可以添加第三道防线:配置设置,以避免在测试/开发环境中执行真正的逻辑。这没什么不对的。但请不要将其命名为IsInUnitTest,因为问题比这更广泛(您还希望避免在开发人员计算机上执行此逻辑)

答案 1 :(得分:0)

AngularJS Unit Testing Documentation实际上非常出色地描述了开发人员处理单元测试的不同方式。

在他们提出的四种不同方法中,他们推荐的方法之一是涉及使用依赖注入,以便您的生产代码流与测试流程相同。唯一的区别是您传递给代码的对象会有所不同。请注意,Claudio使用术语“Stub”来表示您传递给方法的对象,以作为您在生产中使用的占位符:

public Document getPage(String url, AbstractJsoup jsoup) {
  return jsoup.connect(url).get();
}

我的Java有点生疏,所以请考虑你可能需要实际做一些时髦的东西来完成这项工作,比如检查jsoup对象的类型或将它转换为测试Jsoup对象或生产Jsoup对象,可能只是打败了使用依赖注入的整个目的。因此,如果您正在谈论像JavaScript或其他一些松散类型的函数式编程语言这样的动态语言,依赖注入会使事情变得非常干净。

但是在Java中,除非你控制依赖关系并且可以使用设计模式(如strategy pattern)来传递不同的具体实现,否则执行你正在做的事情可能会更容易,特别是因为你没有控制Jsoup代码。但是,您可能会检查它们是否具有可用作占位符的可用存根,因为某些库开发人员会写存根。

如果您不拥有代码,另一个选项可能是使用Factory类来获取所需的对象,具体取决于您在首次实例化时设置的标志。这似乎是一个不太理想的解决方案,因为您仍然在该Factory对象中设置一个可能对您可能不会尝试测试的内容产生影响的全局标志。依赖注入用于单元测试的优点是,您必须在测试时显式传入测试存根,并在希望方法执行写入操作时显式传入生产对象。一旦方法完成执行,测试就结束了,任何调用它的生产过程都会自动在生产模式下运行它,因为它会注入生产对象。

答案 2 :(得分:0)

<强>更新

这是我在发布这个问题后不久的想法,但我现在确信这不是一个好计划。将它留给后人,但请参阅my newer answer了解我最终做的事情。


我很难确定这是正确的做法,但有人认为我至少解决了我的第一和第三个异议,来自Determine if code is running as part of a unit test

您可以通过直接检查执行堆栈来避免存储关于您是否处于单元测试中的外部状态,如下所示:

/**
 * Determines at runtime and caches per-thread if we're in unit tests.
 */
public static class IsInUnitTest extends ThreadLocal<Boolean> {
    private static final ImmutableSet<String> TEST_PACKAGES = 
                                       ImmutableSet.of("org.testng");

    @Override
    protected Boolean initialValue() {
        for(StackTraceElement ste : Thread.currentThread().getStackTrace()) {
            for(String pkg : TEST_PACKAGES) {
                if(ste.getClassName().startsWith(pkg)) {
                    return true;
                }
            }
        }
        return false;
    }
}

这里的主要优点是我们不存储任何州;我们只需检查堆栈跟踪 - 如果跟踪包含测试框架包,我们将进行单元测试,否则我们不会。它并不完美 - 特别是如果你使用相同的测试框架进行集成或其他更宽松的测试,它可能会导致误报 - 但避免外部状态似乎至少是一个轻微的胜利。

好奇其他人对这个想法的看法。

答案 3 :(得分:0)

我从来没有见过一个系统,它采取措施主动阻止在单元测试下运行的代码访问外部资源。问题从未出现过。你对在某个地方工作有最深切的同情。

有没有办法可以控制用于单元测试的类路径,以使访问外部资源所需的库不可用?如果类路径上没有JSoup且没有JDBC驱动程序,则尝试使用它们的代码的测试将失败。您将无法以这种方式排除SocketURLConnection等JDK级别的类,但它可能仍然有用。

如果您使用Gradle运行测试,那将非常简单。如果你使用的是Maven,也许不是。我认为在Eclipse中有任何方法可以使用不同的类路径进行编译和测试。

答案 4 :(得分:0)

嗯,你可以使用Abstract Factory Pattern实现相同的清洁方式(可能不适合称之为Abstract Factory Pattern)。

C#中的示例:

public class CardChargerWrapper{
    public CardChargerWrapper(
        NullCardCharger nullCharger
        , TestUnitConfig config){
        // assign local value
        this.charger = new CardCharger();
    }
    CardCharger charger;
    NullCardCharger nullCharger;
    TestUnitConfig config;

    public Response Charge(CreditCard card, Money amount){
        if(!config.IsUnitTest()) { return charger.connect().charge(card, amount); }
        else { return NullCardCharger.charge(card, amount); }
    }
}

编辑:更改了CardChargerWrapper以使用CardCharger的硬编码实例,而不是注入它。

注意:您可以将NullCardCharger更改为MockCardChargerOfflineCardCharger,以便进行记录。

再次注意:您可以更改CardChargerWrapper的构造函数以使其适合。例如,您可以注入属性,而不是注入NullCardCharger的构造函数。与TestUnitConfig相同。

编辑:关于调用IsUnitTest()一个好主意:

这实际上取决于您的业务视角以及您如何进行测试。正如许多人所说,尚未经过测试的代码因其正确性而不受信任。它不可靠。另外,我更喜欢IsChargeRealCard()IsUnitTest()更合适。

假设我们在上下文中取出unit test,至少您仍需要在测试环境中进行集成测试。你可能想测试类似的东西:

  1. 我想测试信用卡验证(是否真实,等等)。
  2. 我想测试付款方式并查看该卡是否正在收费。作为一个过程,但不是真正的信用卡支付。
  3. 对于第二点,我认为最好的办法是创建一个模拟credit card charger来记录事务。这是为了确保充电正确。它将在测试和开发服务器中进行。

    那么,CardChargerWrapper如何帮助这种情况?

    现在有了CardChargerWrapper,你可以:

    1. NullCardCharger切换到任何模拟卡充电器,以增强您的单元测试。
    2. 使用CardChargerWrapper的所有课程都可以确保他们在充值真卡之前先检查IsUnitTest
    3. 您的开发人员需要使用CardChargerWrapper而非CardCharger,以防止单元测试的开发错误。
    4. 在代码审核期间,您可以找到CardCharger是否在其他课程中使用而不是CardChargerWrapper。这是为了确保代码不会泄漏。
    5. 我不确定,但似乎您可以将主项目的引用隐藏到真实的CardCharger。这将进一步保护您的代码。

答案 5 :(得分:0)

  

如果[isInUnitTest()是反模式],我怎么能继续强制单元测试没有达到外部依赖?

我现在有一个我非常满意的解决方案,这将确保在没有显式启用它们的情况下,不能在测试环境中使用外部依赖项。依赖于外部资源(HTTP请求,数据库,信用卡处理器等)的所有类都将其配置类作为其参数之一,该配置类包含初始化这些对象的必要设置。在真实环境中,传入一个包含所需数据的真实Config对象。在测试环境中,传入模拟,并且在没有显式配置模拟的情况下,对象将无法构造/连接。

例如,我有一个Http连接实用程序:

public class HttpOp {
  public HttpOp(Config conf) {
    if(!conf.isHttpAllowed()) {
      throw new UnsupportedOperationException("Cannot execute HTTP requests in "+
          getClass()+" if HTTP is disabled.  Did you mean to mock this class?");
    }
  }
  ....
}

在单元测试中,如果您尝试运行构造HttpOp对象的代码,则会引发异常,因为模拟的Config将不会返回true,除非明确设置为这样做。在需要这种功能的功能测试中,您可以明确地这样做:

@Test
public void connect() {
  State.Http httpState = mock(State.Http.class);
  when(httpState.isEnabled()).thenReturn(true);
  RemoteDataProcessor rdp = new RemoteDataProcessor(new HttpOp(httpState));
  ...

}

当然,这仍然取决于Config在测试环境中被正确模拟,但现在我们只有一个危险点需要查找,审阅者可以快速验证Config对象是否被嘲笑并相信只有明确启用的实用程序才可访问。同样,现在只有一个新的团队成员需要被告知(“总是在测试中模拟Config”)他们现在可以确信他们不会意外地收取信用卡或向客户发送电子邮件。 / p>