作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
尼古拉斯·费雪的头像

Nickolas Fisher

nicolas专门从事大型企业web应用程序, payment gateways, software architecture, and Windows services.

Expertise

Years of Experience

15

Share

当与涉众和客户讨论单元测试时,通常会有很多困惑和怀疑. 单元测试有时听起来就像用牙线清洁孩子, “我已经刷过牙了,为什么还要刷呢??”

对于那些认为自己的测试方法和用户验收测试足够强大的人来说,建议进行单元测试听起来像是不必要的开销.

But 单元测试是一个非常强大的工具 而且比你想象的要简单. 在本文中,我们将了解单元测试以及DotNet中可用的工具,例如 Microsoft.VisualStudio.TestTools and Moq.

我们将尝试构建一个简单的类库来计算斐波那契数列的第n项. To do that, 我们将创建一个计算斐波那契数列的类,该类依赖于将数字相加的自定义数学类. Then, we can use the .. NET测试框架,以确保我们的程序按预期运行.

What is Unit Testing?

单元测试将程序分解成最小的代码, usually function-level, 并确保函数返回预期的值. 通过使用单元测试框架, 单元测试成为一个独立的实体,可以在程序构建时对其运行自动化测试.

[TestClass]
    公共类FibonacciTests
    {
        [TestMethod]
        //检查计算的第一个值
        Fibonacci_GetNthTerm_Input2_AssertResult1()
        {
            //Arrange
            int n = 2;

            //setup
            Mock mockMath = new Mock();
            mockMath
                .Setup(r => r.Add(It.IsAny(), It.IsAny()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci =新单元测试.Fibonacci(mockMath.Object);

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

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

一个使用Arrange的简单单元测试, Act, 断言我们的数学库可以正确地添加2 + 2的方法学测试.

单元测试设置完成后, 如果对代码进行了更改, 为了解释该程序最初开发时不知道的另一种情况, for example, 单元测试将显示是否所有情况都与函数输出的期望值相匹配.

Unit testing is not integration testing. It is not end-to-end testing. 虽然这两种方法都很强大, 它们应该与单元测试一起工作,而不是作为替代品.

单元测试的好处和目的

这是单元测试最难理解的好处, but the most important, 是否能够动态地重新测试更改后的代码. 它之所以如此难以理解,是因为许多开发人员认为, “我再也不会碰那个功能了。” or “我做完后会重新测试的.” 利益相关者会这样想, “如果那篇文章已经写好了,为什么我还需要重新测试它?”

作为一个经历过两方面发展的人,这两方面我都说过. 我体内的开发人员知道我们为什么要重新测试它.

我们每天所做的改变会产生巨大的影响. For example:

  • 您的开关是否正确地考虑了您输入的新值?
  • 你知道你用了多少次那个开关吗?
  • 您是否正确地考虑了不区分大小写的字符串比较?
  • 您是否正确地检查了空值?
  • throw异常是否按预期得到处理?

单元测试处理这些问题,并将它们记录在代码和代码中 process 以确保这些问题总是得到回答. 可以在构建之前运行单元测试,以确保没有引入新的错误. 因为单元测试被设计为原子性的,所以它们运行得非常快, 每次测试通常少于10毫秒. 即使在一个非常大的应用程序中,一个完整的测试套件也可以在一个小时内完成. 您的UAT流程能匹配吗?

设置一个命名约定的示例,以便在要测试的类中轻松搜索类或方法.

作为一名开发者,也许这听起来更像是你的工作. 是的,你可以放心,因为你发布的代码是好的. 但是,单元测试也为您提供了看到设计弱点的机会. 您是否为两段代码编写相同的单元测试? 它们应该放在一段代码上吗?

Getting your 代码是可单元测试的 它本身就是一种改进设计的方法. 对于大多数从未进行过单元测试的开发人员来说, 或者在编码之前不要花太多时间考虑设计, 通过为单元测试做好准备,您可以意识到您的设计改进了多少.

你的代码单元可测试吗?

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

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

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

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

如果测试中的方法需要另一个类或函数,我们称之为依赖项. In unit testing, we don’t care what the dependency is doing under the hood; for the purpose of the method under test, it is a black box. 依赖项有自己的一组单元测试,用于确定其行为是否正常工作.

As a tester, 您希望模拟该依赖项,并告诉它在特定实例中返回哪些值. 这将使您更好地控制您的测试用例. To do this, 你需要注入一个假人(或者我们稍后会看到), 该依赖的模拟版本.

组件之间的交互是否符合您的期望?

一旦您确定了依赖项和依赖注入, 您可能会发现在代码中引入了循环依赖. 如果A类依赖于B类,而B类又依赖于A类,那么您应该重新考虑您的设计.

依赖注入之美

Let’s consider our Fibonacci example. 你的老板告诉你,他们有一个新的类,比c#中现有的添加操作符更高效、更准确.

虽然这个特殊的例子在现实世界中不太可能出现, 我们确实在其他组件中看到类似的例子, such as authentication, object mapping, 以及几乎所有的算法过程. 为本文的目的, 让我们假设您的客户端的新添加功能是自计算机发明以来最新最好的.

因此,您的老板交给您一个带有单个类的黑盒库 Math,在那个类中,一个函数 Add. 你实现斐波那契计算器的工作可能看起来像这样:

 public gettnthterm (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;
        }

This isn’t horrendous. You instantiate a new Math 类,然后用它将前两项相加得到下一项. 通过常规的一系列测试运行此方法, 计算出100项, 计算第1000项, the 10,000th term, 以此类推,直到您对自己的方法运行良好感到满意为止. 然后在将来的某个时候,用户抱怨第501项没有像预期的那样工作. 您花了一个晚上的时间检查您的代码,并试图找出为什么这个极端情况不能工作. 你开始怀疑最新最好的 Math 课堂并不像你老板想的那么好. 但这是一个黑盒子,你无法证明你在内部陷入了僵局.

这里的问题是依赖关系 Math 并没有注入你的斐波那契计算器. 因此,在您的测试中,您总是依赖于现有的、未测试的和未知的结果 Math 来测试斐波那契. 如果有什么问题 Math,那么斐波那契将永远是错误的(没有为第501项编写特殊情况).

纠正这个问题的方法是注入 Math 类放入斐波那契计算器中. 但更好的是,创建一个接口 Math 类,它定义了公共方法(在本例中, Add),并在我们的 Math class.

 public interface IMath
    {
        int Add(int x, int y);
    }
    公共类数学:IMath
    {
 public int Add(int x, int y)
        {
            //在这里实现超级秘密
        }
    }
} 

Rather than inject the Math 类中,我们可以注入 IMath 接口到斐波那契. 这里的好处是我们可以定义我们自己的 OurMath 类,我们知道它是准确的,然后用它来测试计算器. 更好的是,使用Moq我们可以简单地定义什么 Math.Add returns. 我们可以定义一些和或者我们可以告诉 Math.Add to return x + y.

        private IMath _math;
        斐波那契数列(IMath数学)
        {
            _math = math;
        } 

将IMath接口注入Fibonacci类

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

使用Moq来定义什么 Math.Add returns.

现在我们有了一个久经考验的方法, 如果+运算符在c#中是错误的,我们有更大的问题)方法来添加两个数字. Using our new Mocked IMath, 我们可以为第501个学期编写一个单元测试,看看我们的实现是否出错,或者自定义是否出错 Math 这个类还需要再多做一些工作.

不要让一个方法尝试做太多的事情

这个例子也指出了方法做得太多的想法. Sure, 加法是一个相当简单的操作,不需要将其功能从我们的 GetNthTerm method. 但是如果手术更复杂一些呢? Instead of addition, 也许这是模型验证, 调用工厂来获取要操作的对象, 或者从存储库中收集额外需要的数据.

大多数开发人员都会坚持一种方法有一个目的的想法. In unit testing, 我们试图坚持单元测试应该应用于原子方法的原则,通过向方法引入太多的操作,我们使其无法测试. 我们经常会产生这样一个问题:我们必须编写如此多的测试来正确地测试我们的函数.

我们向方法中添加的每一个参数都会根据参数的复杂性以指数方式增加我们必须编写的测试的数量. 如果你在逻辑中加入一个布尔值, 您需要将要编写的测试的数量增加一倍,因为您现在需要与当前的测试一起检查true和false用例. 在模型验证的情况下,我们的单元测试的复杂性会迅速增加.

向逻辑中添加布尔值时所需增加的测试的图表.

我们都会为在方法中添加一些额外的东西而感到内疚. 但是这些更大、更复杂的方法需要太多的单元测试. 当您编写单元测试时,很快就会发现该方法试图做的事情太多了. 如果你觉得你试图从输入参数中测试太多可能的结果, 考虑到您的方法需要被分解成一系列较小的方法.

Don’t Repeat Yourself

我们最喜欢的程序员之一. 这个应该是相当直接的. 如果您发现自己不止一次地编写相同的测试,那么您就不止一次地引入了代码. 将该工作重构为一个公共类,使您尝试使用它的两个实例都可以访问它,这可能对您有好处.

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

DotNet为我们提供了一个非常强大的单元测试平台. 使用它,您可以实现所谓的 安排、行动、主张方法论. 你安排你最初的考虑, 在这些条件下使用你的测试方法, 然后断言发生了什么事. 您可以断言任何东西,使该工具更加强大. 您可以断言某个方法被调用了特定次数, 该方法返回一个特定的值, 抛出了特定类型的异常, 或者任何你能想到的东西. 对于那些寻找更高级框架的人来说,NUnit和它的Java对应物 JUnit are viable options.

[TestMethod]

        //测试以验证Add从未在第一个Term上调用
        公共无效Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled()
        {

            //Arrange
            int n = 0;

            //setup
            Mock mockMath = new Mock();
            mockMath
                .Setup(r => r.Add(It.IsAny(), It.IsAny()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci =新单元测试.Fibonacci(mockMath.Object);

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

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

通过抛出异常测试斐波那契方法是否处理负数. 单元测试可以验证是否抛出了异常.

要处理依赖注入,两者都有 Ninject and Unity 存在于DotNet平台上. 这两者之间的差别很小, 这就变成了一个问题,如果你想用流畅语法或XML配置来管理配置.

为了模拟依赖关系,我推荐使用Moq. Moq可能很难上手, 但要点是创建依赖项的模拟版本. 然后,告诉依赖项在特定条件下返回什么. 例如,如果您有一个名为 Square(int x) 这个整数的平方,你可以告诉它当x = 2时,返回4. 你也可以让它对任意整数返回x^2. 或者你可以让它在x = 2时返回5. 你为什么要做最后一个病例? 在这种情况下,测试角色下的方法是验证依赖项的答案, 您可能希望强制返回无效的答案,以确保正确捕获错误.

 [TestMethod]

        //测试验证添加调用三次在第五个Term
        Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes()
        {

            //Arrange
            int n = 4;

            //setup
            Mock mockMath = new Mock();
            mockMath
                .Setup(r => r.Add(It.IsAny(), It.IsAny()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci =新单元测试.Fibonacci(mockMath.Object);

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

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

用Moq来告诉被嘲笑的人 IMath interface how to handle Add under test. 可以设置显式用例 It.Is or a range with It.IsInRange.

DotNet的单元测试框架

微软单元测试框架

The 微软单元测试框架 来自微软的开箱即用的单元测试解决方案是否包含在Visual Studio中. 因为它是和VS一起来的,它和VS很好地集成在一起. 当你开始一个项目, Visual Studio会询问你是否想在你的应用程序中创建一个单元测试库.

微软单元测试框架还附带了一些工具来帮助您更好地分析您的测试 testing procedures. Also, 因为它是由微软拥有和编写的, 它的存在会给人一种稳定的感觉.

但是当你使用微软的工具时,你会得到他们给你的东西. 微软单元测试框架集成起来很麻烦.

NUnit

对我来说最大的好处是 NUnit is parameterized tests. 在上面的斐波那契例子中, 我们可以输入一些测试用例,并确保这些结果是真实的. 在第501题中, 我们总是可以添加一个新的参数集,以确保测试总是运行,而不需要新的测试方法.

NUnit的主要缺点是将它集成到Visual Studio中. 它缺少微软版本的功能,这意味着您需要下载自己的工具集.

xUnit.Net

xUnit 在c#中非常流行,因为它与现有的 .NET ecosystem. Nuget有很多xUnit的扩展. 它还与Team Foundation Server集成得很好,尽管我不确定集成了多少 .NET developers 仍然在各种Git实现上使用TFS.

缺点是,许多用户抱怨xUnit的文档有点缺乏. 对于单元测试的新用户来说,这可能会引起很大的头痛. In addition, xUnit的可扩展性和适应性也使得学习曲线比NUnit或微软的单元测试框架更陡峭.

测试驱动设计/开发

测试驱动设计/开发(TDD) 是否有更高级的话题值得单独发表. 不过,我还是想介绍一下.

这个想法是从单元测试开始,并告诉单元测试什么是正确的. 然后,您可以围绕这些测试编写代码. In theory, 这个概念听起来很简单, but in practice, 训练你的大脑向后思考应用程序是非常困难的. 但是这种方法有一个内在的好处,那就是不需要在事后编写单元测试. 这减少了重构、重写和类混淆.

近年来,TDD已经成为一个流行词,但采用速度很慢. 它的概念性质让利益相关者感到困惑,这使得它很难获得批准. But as a developer, 我鼓励您使用TDD方法编写一个小的应用程序,以适应这个过程.

为什么不能有太多的单元测试

单元测试是最强大的测试之一 testing tools 开发者可以随意使用. 对于您的应用程序的全面测试,这是远远不够的, 但是它在回归测试中的好处, code design, 目的的文件是无与伦比的.

编写过多的单元测试是不可能的. 每个边缘情况都可能在您的软件中提出大问题. 将发现的bug作为单元测试进行记忆,可以确保这些bug在以后的代码更改过程中不会再回到您的软件中. 虽然你可以在项目的前期预算中增加10-20%, 你可以在训练中节省更多的钱, bug fixes, and documentation.

您可以找到本文中使用的Bitbucket存储库 here.

了解基本知识

  • 单元测试是什么意思?

    单元测试测试单个方法的过程. 单元测试确保每个组件按预期工作.

  • 单元测试的例子是什么?

    单元测试通过将已知值传递给原子函数并断言函数产生的预期结果来测试原子函数的过程.

  • 单元测试是如何执行的?

    单元测试是通过使用框架和工具集来执行的,这些框架和工具集通常是针对代码库的框架量身定制的.

  • 什么是好的单元测试?

    一个好的单元测试包含了方法中所有可能的输入. 设计人员应该知道这些方法的结果,单元测试应该反映预期的结果.

  • 单元测试的好处是什么?

    单元测试通过要求您编写可测试的代码来帮助确保代码的质量, 它确保了代码的功能性, 它为回归测试提供了一个很好的起点.

  • 单元测试值得吗?

    Yes. 单元测试可能会增加您的开发成本,但从长远来看, 它们将通过回归测试和错误记忆节省您的时间和精力.

  • 我应该写多少个单元测试?

    理想情况下,单元测试应该涵盖代码的所有方面. Of course, in reality, 这是不可能的,简单的答案是你永远不能写太多的单元测试.

聘请Toptal这方面的专家.
Hire Now
尼古拉斯·费雪的头像
Nickolas Fisher

Located in 西切斯特,宾夕法尼亚州,美国

Member since May 16, 2019

About the author

nicolas专门从事大型企业web应用程序, payment gateways, software architecture, and Windows services.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

15

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.