星驰编程网

免费编程资源分享平台_编程教程_代码示例_开发技术文章

单元测试私有方法

当涉及单元测试时,最常见的问题之一是:如何测试私有方法?

单元测试私有方法

我认为单元测试中最大的误解之一就是这种观念,即在测试类时,应该使用单元测试覆盖其中的每个方法。这种想法的逻辑扩展是也尝试用测试覆盖私有方法。只是将它们公开-没什么大不了的!-并完成它。

好吧,这是单元测试的可怕方法。通过公开本来可以保密的方法,您可以揭示实现细节。通过将测试耦合到那些实现细节,您将获得一个脆弱的单元测试套件。

当您的测试开始对被测系统(SUT)的内部了解太多时, 在重构期间会导致 误报。这意味着它们将不再充当安全网,而将阻碍您的重构工作,因为必须重构它们以及它们所绑定的实现细节。基本上,他们将停止实现其主要目标:为您提供代码正确性的信心。

在进行单元测试时,您需要遵循以下规则:仅测试SUT的公共API,不要公开其实现细节以启用单元测试。您的测试应该像普通客户一样使用SUT,不要给他们任何特殊的特权。在这里,您可以详细了解什么是实现细节以及它与public API:link的不同之处。

那么,如何测试私有方法的问题的答案是:没有。只是不要那样做。让SUT按照所需的方式整理其行为,仅测试该行为的可观察结果。SUT的客户可以观察到的行为应该是唯一的!-测试中验证的目标。

但是,如果私有方法过于复杂并且未经测试就太危险了怎么办?如果某些私有方法中的逻辑过多,并且无法通过SUT的公共API对其进行单元测试,该怎么办?这表明您在这里错过了抽象。不必公开此方法,而是将其内部工作原理提取到一个单独的类中并测试该类。

看下面的例子:

public class Order
{
    private Customer _customer;
    private List<Product> _products;

    public string GenerateDescription()
    {
        return #34;Customer name: {_customer.Name}, " +
            #34;total number of products: {_products.Count}, " +
            #34;total price: {GetPrice()}";
    }

    private decimal GetPrice()
    {
        decimal basePrice = /* Calculate based on _products */;
        decimal discounts = /* Calculate based on _customer */;
        decimal taxes = /* Calculate based on _products */;
        return basePrice - discounts + taxes;
    }
}

在这里,Order的GenerateDescription() 本身非常简单:它只返回Order的一些一般性描述。但是它使用的私有GetPrice() 更为复杂:它包含重要的业务逻辑,需要对其进行彻底的测试。这种复杂性是隐藏抽象的有力标志。

您可以引入一个单独的域概念PriceCalculator,而不是公开此方法:

public class Order
{
    private Customer _customer;
    private List<Product> _products;

    public string GenerateDescription()
    {
        var calculator = new PriceCalculator();

        return #34;Customer name: {_customer.Name}, " +
            #34;total number of products: {_products.Count}, " +
            #34;total price: {calculator.Calculate(_customer, _products)}";
    }
}

public class PriceCalculator
{
    public decimal Calculate(Customer customer, List<Product> products)
    {
        decimal basePrice = /* Calculate based on products */;
        decimal discounts = /* Calculate based on customer */;
        decimal taxes = /* Calculate based on products */;
        return basePrice - discounts + taxes;
    }
}

现在可以与分开测试该新类Order。而且,您还可以在这里使用单元测试的功能样式,因为此类本身并不维护任何内部状态:它根据提供的输入生成输出。

单元测试内部课程

单元测试专用方法的问题通常会引起另一个相关问题。那就是:如何测试内部类?它们的情况不像私有方法那样简单。

让我们用Order 和PriceCalculator 类扩展示例,并说我们要进行PriceCalculator 内部处理,因为在域模型之外的任何地方都没有使用它。全部位于单个装配件中。

这是一个合理的决定。最好将域模型的API表面保持尽可能小,不要不必要地扩展它。

我们可以InternalsVisibleTo 在域程序集上使用该属性,并使内部类对单元测试可见。但这是否会带来与私有方法相同的问题?换句话说,难道我们不将测试与内部实现细节耦合起来并使它们变得脆弱吗?毕竟,在使用的域模型之外没有客户端PriceCalculator,那么为什么我们要为单元测试提供特殊的特权呢?

这是一个公平的问题。答案是:不,我们不会将测试与实现细节耦合在一起。

看看为什么让我们看一下我先前提出的两个要求:

  • 单元测试应仅使用SUT的公共API。
  • 单元测试应模仿SUT客户的行为。

虽然它PriceCalculator 本身是内部的,但它仍然具有公共API和使用该API的客户端。计算器和它的客户都位于同一程序集中。

我们可以将这种情况描述如下:


此处的域模型表示为绿色圆圈。尽管没有外部客户端在使用它,但是域模型本身由多层组成,并且外层成员利用内层来实现其目标。它们充当与那些内部层的成员有关的客户端。

因此,为了对内层成员进行单元测试,我们可以使用域模型中客户端使用的相同公共API。在使用Order 和的情况下PriceCalculator,我们可以针对 该类编写测试,PriceCalculator.Calculate() 因为这是Order该类调用的API 。

PriceCalculator 和单元测试位于单独的组件中的事实在这里仅仅是技术上的不便。这不应阻止我们对该类进行单元测试。尽管是内部的,它仍然具有我们可以在测试中绑定的公共API。

我个人甚至不再将这些类设置为内部类,尽管它们在同一程序集中没有“普通”客户。它以扩大域模型的API面为代价,简化了单元测试,但我发现这种权衡值得做。

摘要

  • 不要针对SUT中的每种方法进行单元测试。仅对公开可用的API进行单元测试。
  • 在编写单元测试时,应模仿SUT客户端的行为。
  • 不要测试私有方法。要么使用公共API对单元进行间接测试,要么将它们提取到单独的类中,然后测试这些类。
  • 只要您遵循上述准则,请不要犹豫对内部测试进行单元测试。

原文地址:
https://enterprisecraftsmanship.com/posts/unit-testing-private-methods/

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言