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

问题描述 投票:6回答:6

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

我公司的代码库充满了这样的代码:

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()有代码味吗?
  • 如果是这样,我怎么能继续强制单元测试不能达到外部依赖?
  • 如果没有,那么实施这种方法的最佳方法是什么,以及何时使用/避免它的好方法是什么?

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

unit-testing design-patterns language-agnostic anti-patterns
6个回答
5
投票

IsInUnitTests()是一个代码味道吗?

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

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

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

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

编辑

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

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

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

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

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

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

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

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

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

简而言之:

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

0
投票

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

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

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

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

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

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


0
投票

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

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

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


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. 我想测试付款方式,看看该卡是否正在收费。作为一个过程,但不作为真正的信用卡支付。

对于第二点,我认为最好的办法是创建一个模拟credit card charger来记录事务。这是为了确保充电正确。它将在测试和开发服务器中进行。

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

现在使用CardChargerWrapper,您可以:

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

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”)他们现在可以确信他们不会意外地收取信用卡或向客户发送电子邮件。


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;
    }
}

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

好奇别人对这个想法的看法。

© www.soinside.com 2019 - 2024. All rights reserved.