单元测试及NUnit测试框架简介(二)

一、单元测试框架的选择

在选择单元测试框架时,可以从以下几个方面去考虑:

  • 支持自动检测注册用例:框架能否支持简单地构造用例并自动注册测试用例到测试框架中;
  • 支持测试Fixture:即是否支持为一组测试用例建立统一的脚手架,方便测试用例的上下文构造;
  • 强大的断言系统:是否提供强大的断言系统,供使用者在用例中描述期望;
  • 灵活的Test Suite定义:可以支持灵活的对测试用例分组;
  • 测试能力:是否支持异常测试以及参数测试;
  • 测试filter定义:可以支持灵活的命令行参数,对运行用例进行分组和过滤;
  • 测试结果及报表生成:是否可以生成易于阅读的测试结果报告以及报表文件;
  • 用例依赖管理:是否支持编辑用例的依赖关系,让用例之间互相组合,但是又不破坏每个用例的独立性;
  • 沙盒模式:是否支持测试用例的沙盒模式,降低每个测试用例上下文清理的工作;
  • 是否开源:包括公开的文档和社区的支持是否全面;

常用的C#单元测试框架有xUnit.Net, NUnit, MSTest(Visual Studio),以下主要介绍NUnit测试框架。

二、NUnit测试框架简介

NUnit是一个开源的单元测试框架,用于.NET framework和Mono框架下进行的单元测试。与JUnit在Java中的作用相同,是xUnit家族中的一员。

三、NUnit测试框架安装(Visual Studio)

NUnit框架安装有以下几种方式:

  1. NUnit完整版安装(通过NuGet)
  2. NUnitLite安装(通过NuGet)
  3. 下载Zip和/或MSI文件
  4. 结合的方法

以下介绍前两种安装方法:

1. NUnit完整版安装(通过NuGet)

  1. 在Visual Studio界面,工具 -> NuGet包管理器 -> 管理解决方案的NuGet程序包;
    单元测试及NUnit测试框架简介(二)_第1张图片

  2. 找到并选择NUnit包,在右侧勾选要添加的项目(测试项目,通常为dll),点击安装。Visual Studio会将包下载到解决方案所在目录的packages文件夹下,并在勾选的项目中引用NUNit framework;
    单元测试及NUnit测试框架简介(二)_第2张图片

  3. NUnit单元测试可以使用NUnit框架自带的runner运行,也可以集成到Visual Studio的测试平台上,使用Microsoft提供的的runner运行。这两种方式需要安装不同的NuGet包:

    使用NUnit框架自带的runner:在NuGet包管理器中搜索NUnit.Console,并安装。安装完之后,就可以在packages\NUnit.ConsoleRunner.3.X.X\tools中找到unit3-console并运行;

    使用Microsoft提供的的runner:在NuGet包管理器中搜索NUnit3TestAdapter,并安装;

  4. 确认测试项目引用了nunit.framework。

2. NUnitLite安装(通过NuGet)

NUnitLite是一种轻量化的安装方式,不用安装NUnit runner和test engine assemblies。仅需要安装NUnit framework和一个小型的runner即可。

  1. 创建一个控制台程序项目作为测试项目;
  2. 与NUnit完整版安装类似,在NuGet包管理器中搜索并安装NUnit和NUnitLite;
  3. 安装完之后,确认测试项目引用了nunit.framework和nunitlite;
  4. 一个"Program.cs"的文件会被拷贝到测试项目中,里面包括一个程序入口函数Main(),如果测试项目中已存在Main()函数,可以删掉这个文件,但是需要按照下面的格式配置Main()函数,来启动nunitlite runner;
public static int Main(string[] args)
{
  return new AutoRun().Execute(args);
}
  1. 写好测试用例后,启动测试项目就可以了。

四、NUnit框架常用Attributes

[TestFixture]
标识一个测试类

[Test]
标识一个测试方法(对于非参数化的测试,为一个测试用例)

[TestFixture]
public class SuccessTests
{
  // A simple test
  [Test]
  public void Add()
  { /* ... */ }
}

[TestCase]
在参数化测试中,标识一个测试用例(可以包含输入和预期数据)

[TestCase(12, 3, 4)]
[TestCase(12, 2, 6)]
[TestCase(12, 4, 3)]
public void DivideTest(int n, int d, int q)
{
    Assert.AreEqual(q, n / d);
}
[TestCase(12, 3, ExpectedResult=4)]
[TestCase(12, 2, ExpectedResult=6)]
[TestCase(12, 4, ExpectedResult=3)]
public int DivideTest(int n, int d)
{
    return n / d;
}

[TestCaseSource]
在参数化测试中,标识一个测试用例的参数来源

public class MyTestClass
{
    [TestCaseSource(nameof(DivideCases))]
    public void DivideTest(int n, int d, int q)
    {
        Assert.AreEqual(q, n / d);
    }

    static object[] DivideCases =
    {
        new object[] { 12, 3, 4 },
        new object[] { 12, 2, 6 },
        new object[] { 12, 4, 3 }
    };
}
public class MyTestClass
{
    [TestCaseSource(typeof(DivideCases))]
    public void DivideTest(int n, int d, int q)
    {
        Assert.AreEqual(q, n / d);
    }
}

class DivideCases : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return new object[] { 12, 3, 4 };
        yield return new object[] { 12, 2, 6 };
        yield return new object[] { 12, 4, 3 };
    }
}

[TestFixtureSource]
在参数化测试中,为一个测试脚手架设置参数来源,通过构造函数的参数传递

[TestFixtureSource(nameof(FixtureArgs))]
public class MyTestClass
{
    public MyTestClass(string word, int num) { ... }

    /* ... */

    static object [] FixtureArgs = {
        new object[] { "Question", 1 },
        new object[] { "Answer", 42 }
    };
}

[Setup]
在一个TestFixture中,提供运行测试用例之前的统一设置,每次运行一个测试用例之前就会调用一次。

[TearDown]
在一个TestFixture中,提供运行测试用例之后的统一操作,每次运行一个测试用例之后就会调用一次。

namespace NUnit.Tests
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class SuccessTests
  {
    [SetUp] public void Init()
    { /* ... */ }

    [TearDown] public void Cleanup()
    { /* ... */ }

    [Test] public void Add()
    { /* ... */ }
  }
}

[OneTimeSetUp]
在一个TestFixture中,提供运行测试用例之前的统一设置,运行TestFixture中任意一个测试用例之前会调用一次。

[OneTimeTearDown]
在一个TestFixture中,提供运行测试用例之后的统一操作,运行TestFixture中任意一个测试用例之后会调用一次。

[Ignore]
标识一个测试类或方法由于某些原因不应执行,在NUnit3框架中,必须指明原因。

namespace NUnit.Tests
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  [Ignore("Ignore a fixture")]
  public class SuccessTests
  {
    // ...
  }
}

五、NUnit框架的断言系统

NUnit使用两种断言系统,约束模型(Constraint Model)和经典模型(Classic Model)。

经典模型

经典模型对于每一种断言都有单独的一个方法,例如:

StringAssert.AreEqualIgnoringCase("Hello", myString);

Assert类提供一些非常常用的断言方法:

Assert.True
Assert.False
Assert.Null
Assert.NotNull
Assert.Zero
Assert.NotZero
Assert.IsNaN
Assert.IsEmpty
Assert.IsNotEmpty
Assert.AreEqual
Assert.AreNotEqual
Assert.AreSame
Assert.AreNotSame
Assert.Contains
Assert.Greater
Assert.GreaterOrEqual
Assert.Less
Assert.LessOrEqual

除此之外还有以下几个类也提供相应的断言方法:

  • String Assert
  • Collection Assert
  • File Assert
  • Directory Assert

约束模型

约束模型只有Assert.That()一种方法,参数是约束对象。根据不同的断言类型有不同的约束对象,形成一系列重载函数。以下两个断言形式是等价的:

Assert.That(myString, Is.EqualTo("Hello"));
Assert.That(myString, new EqualConstraint("Hello"));

目前NUnit框架仍然支持旧的经典模型,但是已不添加新功能,新功能都是使用约束模型实现的。因此应尽可能的使用约束模型。有些断言形式只能使用约束模型,如:

int[] array = new int[] { 1, 2, 3 };
Assert.That(array, Has.Exactly(1).EqualTo(3));
Assert.That(array, Has.Exactly(2).GreaterThan(1));
Assert.That(array, Has.Exactly(3).LessThan(100));

而本质上经典模型全部是使用约束模型实现的,也就是说所有的经典模型的断言形式都能找到对应的约束模型的形式,比如以下两个断言是等价的:

Assert.AreEqual(4, 2+2);
Assert.That(2+2, Is.EqualTo(4));

多个断言(multiple asserts)

一般来说如果一个函数包含多个断言,一个断言失败了,后续的断言(如果有的话)不会执行,因此,一般一个测试仅会有一个断言。但是某些情况,比如一个UI界面样式的测试可能需要在一个测试方法里添加多个断言,此时可以使用Assert.Multiple方法:

[Test]
public void ComplexNumberTest()
{
    ComplexNumber result = SomeCalculation();

    Assert.Multiple(() =>
    {
        Assert.AreEqual(5.2, result.RealPart, "Real part");
        Assert.AreEqual(3.9, result.ImaginaryPart, "Imaginary part");
    });
}

此时,如果代码块中的第一个断言失败了,测试不会马上终止,而是继续执行第二个断言,最终将结果统一返回。

参考:

  1. NUnit官网.
  2. C/C++怎么做好单元测试.

你可能感兴趣的:(单元测试)