个性化阅读
专注于IT技术分析

.NET单元测试:提前消费,以后再存钱

本文概述

与利益相关者和客户讨论单元测试时, 通常会产生很多困惑和怀疑。有时候, 单元测试听起来像牙线对孩子的样子:”我已经刷牙了, 为什么需要这么做?”

对于认为自己的测试方法和用户接受度测试足够强大的人, 建议进行单元测试通常听起来像是不必要的支出。

但是单元测试是一个非常强大的工具, 比你想象的要简单。在本文中, 我们将研究单元测试以及DotNet中可用的工具, 例如Microsoft.VisualStudio.TestTools和Moq。

我们将尝试构建一个简单的类库, 该类库将计算斐波纳契序列中的第n个项。为此, 我们将要创建一个用于计算斐波那契数列的类, 该类依赖于将数字加在一起的自定义数学类。然后, 我们可以使用.NET测试框架来确保我们的程序按预期运行。

什么是单元测试?

单元测试将程序分解为最小的代码位(通常是功能级别), 并确保该功能返回期望的值。通过使用单元测试框架, 单元测试将成为一个单独的实体, 然后可以在构建程序时对该程序运行自动测试。

[TestClass]
    public class FibonacciTests
    {
        [TestMethod]
        //Check the first value we calculate
        public void Fibonacci_GetNthTerm_Input2_AssertResult1()
        {
            //Arrange
            int n = 2;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            Assert.AreEqual(result, 1);
        }
}

使用Arrange, Act, Assert方法论测试的简单单元测试, 我们的数学库可以正确地加2 + 2。

设置单元测试后, 如果对代码进行了更改, 以解决首次开发程序时未知的其他条件, 例如, 单元测试将显示所有情况是否均与预期值匹配通过函数输出。

单元测试不是集成测试。这不是端到端测试。尽管这两种方法都是强大的方法, 但它们应与单元测试结合使用, 而不是替代。

单元测试的好处和目的

理解单元测试最困难的好处, 但是最重要的是能够动态地重新测试更改的代码。之所以难以理解, 是因为有那么多开发人员对自己进行思考, “我永远不会再碰到该功能”或”完成后我将对其进行重新测试。”利益相关者认为:”如果该部分已经写好了, 为什么还要重新测试呢?”

作为发展领域中的两个方面的人, 我已经谈到了这两个方面。我内心的开发人员知道为什么我们必须重新测试它。

我们每天进行的更改可能会产生巨大的影响。例如:

  • 你的交换机是否正确考虑了你输入的新值?
  • 你知道使用该开关多少次吗?
  • 你是否正确考虑了不区分大小写的字符串比较?
  • 你是否在适当地检查空值?
  • 抛出异常会按预期进行处理吗?

单元测试接受这些问题, 并将其记录在代码和过程中, 以确保始终回答这些问题。可以在构建之前运行单元测试, 以确保你没有引入新的错误。因为单元测试被设计为原子的, 所以它们可以非常快速地运行, 通常每次测试不到10毫秒。即使在非常大的应用程序中, 完整的测试套件也可以在一个小时内执行。你的UAT流程可以匹配吗?

设置命名约定的示例,可以轻松地在要测试的类中搜索类或方法。

除了第一次运行并包含设置时间的Fibonacci_GetNthTerm_Input2_AssertResult1外, 所有单元测试均在5毫秒内运行。我在这里的命名约定是为了轻松地在我要测试的类中搜索类或方法而设置的

不过, 作为开发人员, 也许这听起来对你来说需要更多工作。是的, 你可以放心, 你发布的代码是好的。但是, 单元测试还为你提供了查看设计薄弱之处的机会。你是否要为两个代码编写相同的单元测试?他们应该使用一段代码代替吗?

使代码本身可以进行单元测试是你改进设计的一种方式。对于大多数从未进行过单元测试的开发人员, 或者在编码之前没有花太多时间考虑设计的开发人员, 你可以通过准备进行单元测试来了解设计的改进之处。

你的代码单元可以测试吗?

除了DRY, 我们还有其他考虑。

你的方法或函数尝试做太多吗?

如果你需要编写运行时间超出预期的过于复杂的单元测试, 则你的方法可能过于复杂, 更适合作为多种方法使用。

你是否适当地利用了依赖注入?

如果你的被测方法需要另一个类或函数, 我们将其称为依赖项。在单元测试中, 我们不在乎依赖项是做什么的;就被测方法而言, 它是一个黑匣子。依赖项具有自己的一组单元测试, 这些单元测试将确定其行为是否正常运行。

作为测试人员, 你想模拟该依赖关系, 并告诉它在特定实例中返回什么值。这将使你更好地控制测试用例。为此, 你需要注入该依赖项的虚拟版本(或稍后将看到的模拟版本)。

你的组件是否相互影响, 你的期望如何?

确定依赖性和依赖性注入后, 你可能会发现在代码中引入了循环依赖性。如果A类依赖于B类, 而B又依赖于A类, 则应重新考虑你的设计。

依赖注入之美

让我们考虑我们的斐波那契例子。你的老板告诉你, 他们拥有一个比C#当前可用的add运算符更高效, 更准确的新类。

尽管这个特定示例在现实世界中不太可能出现, 但我们确实在其他组件中看到了类似的示例, 例如身份验证, 对象映射以及几乎所有的算法过程。就本文而言, 我们假设客户的新添加功能是自发明计算机以来最新的功能。

这样, 你的老板将为你提供一个黑盒库, 该库具有单个Math类, 在该类中具有单个Add函数。你实现斐波那契计算器的工作可能看起来像这样:

 public int GetNthTerm(int n)
        {
            Math math = new Math();
            int nMinusTwoTerm = 1;
            int nMinusOneTerm = 1;
            int newTerm = 0;
            for (int i = 2; i < n; i++)
            {
                newTerm = math.Add(nMinusOneTerm, nMinusTwoTerm);
                nMinusTwoTerm = nMinusOneTerm;
                nMinusOneTerm = newTerm;
            }
            return newTerm;
        }

这并不可怕。你实例化一个新的Math类, 并使用它来添加前两个术语以获得下一个。你可以通过常规的一系列测试来运行此方法, 然后计算出100个项, 计算出第1000个项, 第10, 000个项, 依此类推, 直到你对方法论可以令人满意为止感到满意。然后, 在将来的某个时候, 用户会抱怨第501个字词未按预期运行。你花了一整夜的时间浏览代码, 并试图弄清为什么这种极端情况不起作用。你开始怀疑最新和最好的数学课程并不如你的老板想象的那么好。但这是一个黑匣子, 你无法真正证明这一点–你内部陷入僵局。

这里的问题是你的斐波那契计算器没有注入数学依赖项。因此, 在测试中, 你始终依靠Math的现有, 未经测试和未知的结果来测试Fibonacci。如果Math出现问题, 那么Fibonacci永远是错误的(没有为第501个术语编码特殊情况)。

纠正此问题的想法是将数学类注入你的斐波那契计算器中。但更好的是, 为Math类创建一个接口, 以定义公共方法(在我们的示例中为Add), 并在Math类上实现该接口。

 public interface IMath
    {
        int Add(int x, int y);
    }
    public class Math : IMath
    {
 public int Add(int x, int y)
        {
            //super secret implementation here
        }
    }
} 

除了将Math类注入Fibonacci外, 我们还可以将IMath接口注入Fibonacci。这样做的好处是我们可以定义我们自己的OurMath类, 我们知道该类是准确的, 并可以对此进行测试。更好的是, 使用Moq, 我们可以简单地定义Math.Add返回的值。我们可以定义多个和, 也可以只告诉Math.Add返回x + y。

        private IMath _math;
        public Fibonacci(IMath math)
        {
            _math = math;
        } 

将IMath接口注入Fibonacci类

 //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);

使用Moq定义Math.Add返回的内容。

现在, 我们有了一个经过尝试并且正确的方法(如果在C#中该+运算符是错误的, 那么我们会有更大的问题), 以便将两个数字相加。使用我们新的Mocked IMath, 我们可以为第501个术语编写单元测试代码, 并查看是否对我们的实现进行了调试, 或者自定义Math类是否需要做更多的工作。

不要让一种方法尝试做太多

这个例子也指出了方法做太多的想法。当然, 加法是一个相当简单的操作, 无需从GetNthTerm方法中抽象出其功能。但是, 如果操作稍微复杂些怎么办?与其进行补充, 不如说是模型验证, 呼叫工厂以获取要操作的对象或从存储库收集其他所需数据。

大多数开发人员将尝试坚持一种方法具有一种目的的想法。在单元测试中, 我们尝试坚持将单元测试应用于原子方法的原理, 并且通过对方法引入过多的操作, 使该方法不可测试。我们经常会遇到一个问题, 我们必须编写许多测试才能正确测试我们的功能。

我们添加到方法中的每个参数都会增加我们必须根据参数的复杂度以指数方式编写的测试数量。如果在逻辑中添加布尔值, 则需要将要编写的测试数量加倍, 因为现在需要检查当前测试是否为真和假。在模型验证的情况下, 我们的单元测试的复杂性会迅速增加。

将布尔值添加到逻辑时所需的增加测试的图。

我们都对方法增加一点内gui。但是这些更大, 更复杂的方法导致需要太多的单元测试。当你编写单元测试时, 该方法正试图做太多事情, 这很快变得很明显。如果你想尝试从输入参数中测试太多可能的结果, 请考虑以下事实:你的方法需要分成一系列较小的方法。

不要重复自己

我们最喜欢的编程租户之一。这应该是相当简单的。如果发现自己多次编写相同的测试, 则说明你已经多次引入了代码。将你的工作重构为一个公共类, 这对你尝试使用的两个实例都可以访问, 这可能会有所帮助。

有哪些单元测试工具可用?

DotNet为我们提供了非常强大的单元测试平台。使用此方法, 你可以实施所谓的”安排, 行动, 断言”方法。你安排了最初的考虑因素, 使用被测方法对这些条件采取行动, 然后断言某些事情发生了。你可以声明任何内容, 从而使此工具更加强大。你可以断言某个方法被调用了特定的次数, 该方法返回了特定的值, 抛出了特定类型的异常, 或者你可以想到的其他任何东西。对于那些寻求更高级框架的人来说, NUnit及其Java对应的JUnit是可行的选择。

[TestMethod]

        //Test To Verify Add Never Called on the First Term
        public void Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled()
        {

            //Arrange
            int n = 0;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Never);
        }

通过抛出异常来测试我们的斐波那契方法处理负数。单元测试可以验证是否引发了异常。

为了处理依赖注入, DotNet平台上同时存在Ninject和Unity。两者之间几乎没有什么区别, 这取决于你是否要使用Fluent语法或XML配置来管理配置。

为了模拟依赖关系, 我建议Moq。最小起订量可能很难使你动手, 但是要点是你要创建依赖项的模拟版本。然后, 你告诉依赖项在特定条件下要返回的内容。例如, 如果你有一个名为Square(int x)的将整数平方的方法, 则可以告诉它x = 2时返回4。你还可以告诉它对于任何整数返回x ^ 2。或者你可以告诉它在x = 2时返回5。为什么要执行最后一种情况?如果测试所用的方法是验证依赖项中的答案, 则可能需要强制返回无效答案, 以确保正确捕获错误。

 [TestMethod]

        //Test To Verify Add Called Three times on the fifth Term
        public void Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes()
        {

            //Arrange
            int n = 4;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3));
        }

使用Moq告诉模拟的IMath接口如何处理被测Add。你可以使用It.Is设置显式大小写, 也可以使用It.IsInRange设置范围。

DotNet的单元测试框架

Microsoft单元测试框架

Microsoft单元测试框架是Microsoft提供的即用型单元测试解决方案, 包含在Visual Studio中。由于它是VS附带的, 因此可以很好地集成。当你开始一个项目时, Visual Studio会询问你是否要在应用程序旁边创建一个单元测试库。

Microsoft单元测试框架还附带了许多工具, 可以帮助你更好地分析测试过程。而且, 由于它是由Microsoft拥有和编写的, 因此在将来的发展中会有​​些稳定的感觉。

但是, 使用Microsoft工具时, 你会得到它们所提供的。 Microsoft单元测试框架可能很难集成。

NUnit

对我而言, 使用NUnit的最大好处是参数化测试。在上面的斐波那契示例中, 我们可以输入多个测试用例, 并确保这些结果为真。对于第501个问题, 我们总是可以添加一个新的参数集来确保始终运行测试而无需使用新的测试方法。

NUnit的主要缺点是将其集成到Visual Studio中。它缺少Microsoft版本附带的功能, 意味着你需要下载自己的工具集。

xUnit.Net

xUnit在C#中非常流行, 因为它与现有的.NET生态系统很好地集成了。 Nuget具有许多可用的xUnit扩展。它也可以很好地与Team Foundation Server集成在一起, 尽管我不确定有多少.NET开发人员仍在各种Git实现上使用TFS。

不利的一面是, 许多用户抱怨xUnit的文档有点缺乏。对于新用户进行单元测试, 这可能会引起很大的麻烦。此外, xUnit的可扩展性和适应性也使学习曲线比NUnit或Microsoft的Unit Testing Framework更加陡峭。

测试驱动的设计/开发

测试驱动的设计/开发(TDD)是一个更高级的主题, 值得一提。但是, 我想提供一个介绍。

这个想法是从你的单元测试开始, 然后告诉你的单元测试什么是正确的。然后, 你可以围绕这些测试编写代码。从理论上讲, 这个概念听起来很简单, 但是在实践中, 很难训练你的大脑来思考该应用程序。但是这种方法具有内在的好处, 即事实完成后无需编写单元测试。这样可以减少重构, 重写和类混淆。

近年来, TDD一直是个流行语, 但采用速度很慢。它的概念性质使利益相关者感到困惑, 这使其难以获得批准。但是作为开发人员, 我鼓励你甚至使用TDD方法编写一个小型应用程序来熟悉该过程。

为什么单元测试太多

单元测试是开发人员可以使用的最强大的测试工具之一。它不足以对你的应用程序进行全面测试, 但是它在回归测试, 代码设计和目标文档中的优势是无法比拟的。

没有编写太多的单元测试之类的事情。每个边缘情况都会在你的软件中提出很大的问题。将发现的错误记录为单元测试可以确保这些错误不会在以后的代码更改期间找到爬回软件的方法。尽管你可以在项目的前期预算中增加10%到20%, 但你可以节省的费用远远超过培训, 错误修复和文档编制方面的费用。

你可以在此处找到本文中使用的Bitbucket存储库。

赞(0)
未经允许不得转载:srcmini » .NET单元测试:提前消费,以后再存钱

评论 抢沙发

评论前必须登录!