关于C#程序的单元测试

志铭-2020年1月23日 11:49:41

1.单元测试概念

  • 什么是单元测试?mysql

    单元测试(unit testing)是一段自动化的代码,用来调用被测试的方法或类,然后验证基于该方法或类的逻辑行为的一些假设。git

    简而言之说:单元测试是一段代码(一般一个方法)调用另一段代码,随后检验一些假设的正确性。程序员

    在过程化编程中,一个单元就是单个程序、函数、过程等;github

    对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。正则表达式

  • 为何要单元测试?sql

    单元测试的目标是隔离程序部件并证实这些单个部件是正确的。单元测试在软件开发过程的早期就能发现问题。数据库

    在代码重构或是修改的时候,能够根据单元测试快速验证新修改的代码的正确性,换句话说为了方便系统的后期维护升级!编程

    单元测试某种程度上至关于系统的文档。借助于查看单元测试提供的功能和单元测试中如何使用程序单元,开发人员能够直观的理解程序单元的基础API,即提升了代码的可读性!mvc

    如果开发流程按照测试驱动开发则先行编写的单元测试案例就至关于:软件工程瀑布模式中第二阶段——设计阶段的文档
    使用测试驱动开发,能够避免实际开发中编程人员不彻底按照文档规范,由于是基于单元测试设计方法,开发人员不遵循设计要求的解决方案永远不会经过测试。

  • 何时须要单元测试?

    “单元测试一般被认为是编码阶段的附属工做。能够在编码开始以前或源代码生成以后进行单元测试的设计。”——《软件工程:实践者的研究方法》

    对于须要长期维护的项目,单元测试能够说是必须的

    一般来讲,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程当中先后极可能要进行屡次单元测试,以保证没有程序错误;虽然单元测试不是必须的,但也不坏,这牵涉到项目管理的政策决定。

  • 单元测试谁来编写?

    不须要专门的软件测试人员编写测试案例,单元测试一般由软件开发人员编写。

    也正式由于是开发人员本身写单元测试部分,也可让开发者仔细的思考本身方法和接口是否能够更加便于调用

  • 单元测试局限性

    不能发现集成错误、性能问题、或者其余系统级别的问题。单元测试结合其余软件测试活动更为有效。

  • 单元测试框架

    一般在没有特定框架支持下,自行建立一个项目做为单元测试项目彻底是可行的。
    使用单元测试框架,同时配合编辑器VS,编写单元测试相对来讲会简单许多。
    .NET下的单元测试框架:MSTest、NUnit




2.单元测试的原则

根本原则:

  • Automatic(自动化)
    单元测试应该是全自动执行的,而且非交互式的
  • Independent
    单元测试方法的执行顺序可有可无
    单元测试的各个方法之间不该该相互依赖
  • Repeatable
    功能代码不改的前提下,相同的测试代码屡次运行,应该获得相同的结果
  • Self-validating
    单元测试方法只有两个可能的运行结果:经过或失败,没有第三种状况。

其余一些规范:

  • 最理想的状况下,应该尽可能多写测试用例,以保证代码功能的正确性符合预期,具备良好的容错性。若是代码较复杂,条件分支较多,测试用例最好能覆盖全部的分支路径。

  • 实际开发中,没有必要对每个函数都进行单元测试。可是如果一个比较独立的功能(固然也可能这个功能就一个函数),应该对这个功能进行比较详尽的测试。

  • 单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%。

  • 注意一个类中可能有许多方法,咱们不是要把全部的方法的单元测试都写完,在去实现代码,而是写完一个单元测试,就去实现一个方法,是一种快速的迭代

  • 不测试私有方法,由于私有方法不被外部调用,测试意义不大,并且你非要测试,那就要使用反射,比较麻烦。

  • 一个测试只测试一个功能




3.单元测试简单示例

3.1一个简单的手写单元测试实例

为了简洁明了的说明什么是单元测试,首先不使用单元测试框架,自行编写单元测试项目

好比说新建了一个类Calculator用于对数据的计算,

以下只是随便的的写了个方法,方便理解:

public class Calculator
{
    //求一个数的二倍
    public int DoubleValue(int i)
    {
        return i * 2;
    }
}

新建了Calculator类以后,咱们编写单元测试代码对该类中方法进行单元测试:
首先新建一个项目,对待测试的方法所在的项目添加引用,

编写代码,测试ClassLib项目中Calculator类中的DoubleValue()方法

测试DoubleValue(int value),该函数是求一个数的二倍,给其一个参数value=2,则指望其获得的结果是4,如果其余值则说明函数编写是错误的,测试不经过。如果该函数的运行结果和指望的结果同样则运行经过

public static void CalculatorDoubleValueTest()
{  
    //生成一个测试对象的实例
    Calculator obj = new Calculator();
    //设计测试案例
    int value = 2;
    int expected = 4;
        
    //与预期比较
    if (expected == obj.DoubleValue(value))
    {
        Console.WriteLine("测试经过");
    }
    else
    {
        Console.WriteLine($"测试未经过,测试的实际结果是{obj.DoubleValue(value)}");
    }
    Console.ReadKey();
}

经过上面的示例,简单的演示了单元测试是什么,可是实际中通常都是使用已有的单元测试框架。并且测试一个方法为了完备性通常都要到全部的逻辑路径进行测试,因此会对一个方法写多个测试方法。

3.2单元测试框架MSTest

单元测试通常都是使用现成的单元测试框架,关于.net的单元测试框架有许多,常见的有NUnit,MSTest等等。

这里使用VS自带的MStest框架作简单的演示(通常推荐使用NUnit框架:Undone)

演示的案例,继续对上述的Calculator类中的DoubleValue()进行单元测试

注意:一般的作法是为每一个被测项目创建一个测试项目,为每一个被测类创建一个测试类,而且为每一个被测方法至少创建一个测试方法。

新建项目--->选择测试类项目中的单元测试项目,命名为"被测试项目名+Tests"

测试类的命名为“被测试的类+Tests”

测试函数的命名按照 :**[被测方法]_ [测试场景]_[预期行为]** 格式命名

  • 方法名——被测试的方法
  • 测试场景——能产生预期行为的条件
  • 预期行为——在给定条件下,指望被测试方法产生什么结果

固然在VS中也能够在想要测试的函数上右键,建立单元测试,弹出以下窗口,直接点击肯定便可,便可生成默认的单元测试代码模版

新建单元测试

这里先使用默认自带的MSTest框架,使用默认的命名格式,会自动生成相应的测试项目和测试函数格式。

编写单元测试的代码,通常按照如下四步编写:

Arrange:配置测试对象

TestCase:准备测试案例

Act:操做测试对象

Assert:对操做断言

//注意 [TestClass]和[TestClass()],[TestMethod()]和[TestMethod]写法等价
namespace ClassLib.Tests
{
    [TestClass()]//经过标注该特性标签代表该类为测试类
    public class CalculatorTests
    {
        [TestMethod()]//经过标注该特性标签代表该函数为测试函数
        public void DoubleValueTest_DoubleValue_ReturnTrue()
        {
            //Arrange:准备,实例化一个带测试的类
            Calculator obj = new Calculator();

            //Test Case:设计测试案例
            int value = 2;
            int expected = 4;

            //Act:执行
            int actual = obj.DoubleValue(value);

            //Assert:断言
            Assert.AreEqual(expected, actual);
        }
    }
}

点击测试-->运行-->全部测试
或点击测试-->窗口-->测试资源管理器-->运行全部测试

运行

上面运行显示测试经过显示的是绿色的标志,如果测试不经过则会则显示红色标志,在单元测试中有一种“红绿灯”的概念(你是使用其余的单元测试框架也是一样的红绿标志)。

在测试驱动开发的流程中,就是“红灯-->修改-->绿灯-->重构-->绿灯”的开发流程。

注意:我是使用的不是VS Enterprise版本故没法直接查看代码的测试覆盖率,可使用插件OpenCover或NCover等其余工具查看单元测试的覆盖率。

上面只是演示了怎么进行一次单元测试,可是实际中咱们的测试案例不能仅仅一个,因此要添加多个测试,以提升到测试的完备性

如果对须要大量测试案例的,能够把测试数据存放在专门的用于测试使用的数据库中,在测试时经过链接数据库,使用数据库中的数据进行测试

依旧是上面的示例,把大量的测试案例存放在数据库

Id                   Input       Expected
-------------------- ----------- -----------
1                    2           4
2                    6           12
3                    13          26
4                    0           0
5                    -2          -4

单元测试的代码以下

public TestContext TestContext { get; set; }//注意为了获取数据库的数据,咱们要自定义一个TestContext属性
[TestMethod()]
[DataSource("System.Data.SqlClient",
            @"server=.;database=db_Tome1;uid=sa;pwd=shanzhiming",//数据库链接字符串
            "tb_szmUnitTestDemo",//测试数据存放的表
            DataAccessMethod.Sequential)]//对表中的数据测试的顺序,能够是顺序的,也能够是随机的,这里是咱们选择顺序
public void DoubleValueTest_DoubleValue_ReturnTrue()
{
    //Arrange
    Calculator target = neCalculator();
    //TestCase
    int value = Convert.ToInt(TestContext.DataR["Input"]);
    int expected Convert.ToInt(TestContext.DataR["Expected"]);
    //Act
    int actual target.DoubleValu(value);
    //Assert
    Assert.AreEqual(expected, actual);
}

说明:

  1. 特性标签[TestClass] [TestMethod]

    MSTest框架经过标签识别并加载测试

    [TestClass]用来标识包含一个MSTest自动好测试的类,

    [TestMethod]用来标识须要被调用的自动化测试的方法

  2. 特性标签[DataSource]标识用来测试的数据源,其的参数以下:

    • 第一个参数是providername,即便用的数据源的命名空间,其实咱们也是但是使用Excel表格的(菜单“项目”-->添加新的数据源……)参考:CSDN:vs2015数据驱动的单元测试

      providername值参考:

      • "system.data.sqlclient" ----说明使用的是mssqlserver数据库

      • "system.data.sqllite" ----说明使用的是sqllite数据库

      • "system.data.oracleclient" ----说明使用的是oracle数据库或

      • "mysql.data.mysqlclient" ----说明使用的是mysql数据库

    • 第二个参数是connectionString,我习惯是这样写:

      @"server=.;database=数据库;uid=用户ID;pwd=密码"

      可是推荐这样写:

      @"Data Source=localhost;Initial Catalog=数据库;User ID=用户ID;Password=密码"

    • 第三个参数是tablename,选择使用的数据库中的哪张表

    • 第四个参数肯定对表中的数据测试的顺序.
      能够是顺序的:DataAccessMethod.Sequential
      能够是随机的:DataAccessMethod.Random




4.单元测试框架特性标签

在MSTest单元测试框架中主要有如下的一些特性标签:

(参考)

MS Test Attribute 用途
[TestClass] 定义一个测试类,里面能够包含不少测试函数和初始化、销毁函数(如下全部标签和其余断言)。
[TestMethod] 定义一个独立的测试函数。
[ClassInitialize] 定义一个测试类初始化函数,每当运行测试类中的一个或多个测试函数时,这个函数将会在测试函数被调用前被调用一次(在第一个测试函数运行前会被调用)。
[ClassCleanup] 定义一个测试类销毁函数,每当测试类中的选中的测试函数所有运行结束后运行(在最后一个测试函数运行结束后运行)。
[TestInitialize] 定义测试函数初始化函数,每一个测试函数运行前都会被调用一次。
[TestCleanup] 定义测试函数销毁函数,每一个测试函数执行完后都会被调用一次。
[AssemblyInitialize] 定义测试Assembly初始化函数,每当这个Assembly中的有测试函数被运行前,会被调用一次(在Assembly中第一个测试函数运行前会被调用)。
[AssemblyCleanup] 定义测试Assembly销毁函数,当Assembly中全部测试函数运行结束后,运行一次。(在Assembly中全部测试函数运行结束后被调用)
[Ignore] 跳过(忽略)该测试函数
[TestCategory("测试类别")] 给测试自定义分类,便于有选择的运行指定类别的单元测试

说明:

  • 使用[ClassInitialize][ClassCleanup]标签特性

    能够在测试以前或以后方便地控制测试的初始化和清理,从而确保全部的测试都是使用新的未更改的状态。

    注意,这是颇有必要的,能够有效的防止测试失败是由于测试之间的依赖性致使失败。

    注意两个标签特性须要放在一个无返回值的静态方法上,

    且标注[ClassInitialize]特性的方法的参数是:TestContext testcontext

    示例:好比说在一个测试类初始化一个测试对象,并在测试完成后释放,代码以下:

[TestClass()]
public class CalculatorTests
{
    //使用ClassInitialize标签初始化一个Calculator对象以供下面全部的测试([ClassCleanup]以前)使用
    private static Calculator calc = null;
    [ClassInitialize]
    public static  void  ClassInit(TestContext testcontext)
    {
        calc = new Calculator();
    }

    [TestMethod()]
    public void testMethod1()
    {
         //测试
    }
    [TestMethod()]
    public void testMethod2()
    {
        //测试
    }
    [TestMethod()]
    public void testMethod3()
    {
        //测试
    }
     
    [ClassCleanup]
    public static  void Classup()
    {
        calc = null;
    }
}




5.单元测试中的断言Assert

  1. 断言是什么?能够从字面理解是“十分确定的说”,在编程中能够经过 不一样的断言来测试方法实际运行的结果和你指望的结果是否一致。

  2. 断言是单元测试最基本的组成部分,Assert类的静态方法提供了不一样形式的多种断言。
    MStest中Assert的经常使用静态方法:(参考):

    MS Test Assert 用途
    Assert.AreEqual() 验证值相等
    Assert.AreNotEqual() 验证值不相等
    Assert.AreSame() 验证引用相等
    Assert.AreNotSame() 验证引用不相等
    Assert.Inconclusive() 暗示条件还未被验证
    Assert.IsTrue() 验证条件为真
    Assert.IsFalse() 验证条件为假
    Assert.IsInstanceOfType() 验证明例匹配类型
    Assert.IsNotInstanceOfType() 验证明例不匹配类型
    Assert.IsNotNull() 验证条件为NULL
    Assert.IsNull() 验证条件不为 NULL
    Assert.Fail() 验证失败
  3. 针对字符串的断言,使用StringAssert的静态方法:

    注意能够根据VS的只能提示自行查看StringAssert的全部静态方法,或是查看StringAssert的定义,能够查看其全部的静态方法

    详细使用可参考

    StringAssert 用途
    StringAssert.AreEqualIgnoringCase(string expected,string actual) 用于断言 两个字符串在不区分大小写状况下是否相等,须要提供两个参 数,第一个是期待的结果,第二个是实际结果.
    StringAssert.Contains() 用于断言一个字符串是否包含另外一字符串,其中第一个参数为被包含的字符串,第二个为实际字符串
    StringAssert.StartsWith() 断言字符串是否以某(几)字符开始, 第一个参数为开头的字符串 ,第二个为实际字符串
    StringAssert.EndsWith() 断言字符串是否以某(几)字符结束
    StringAssert.Matches() 断言字符串是否符合特定的正则表达式
  4. 针对集合的断言,使用CollectionAssert的静态方法:

    注意能够根据VS的只能提示自行查看CollectionAssert全部的静态方法,或是查看CollectionAssert的定义,能够查看其全部的静态方法

    详细使用可参考

    CollectiongAssert 用途
    CollectionAssert.AllItemsAreNotNull 断言集合里的元素所有不是Null,也即集合不包含null元素,这个方法只有一个参数,传入咱们要判断的集合便可
    CollectionAssert.AllItemsAreUnique 断言集合里面的元素所有是唯一的,即集合里没有重复元素.
    CollectionAssert.AreEqual 用于断言两个集合是否相等
    CollectionAssert.AreEquivalent 用来判断两个集合的元素是否等价,若是两个集合元素类型相同,个数也相同,即视为等价,与上面的AreEqual方法相比,它不关心顺序
    CollectionAssert.Contains 断言集合是否包含某一元素
    CollectionAssert.IsEmpty 断言某一集合是空集合,即元素个数为0
    CollectionAssert.IsSubsetOf 判断一个集合是否为另外一个集合的子集,这两个集合没必要是同一类集合(能够一个是array,一个是list),只要一个集合的元素彻底包含在另外一个集合中,即认为它是另外一个集合的子集




6.单元测试中验证预期的异常

如果程序中在某种特定的条件下有异常抛出,为了进行单元测试,咱们设计指定的测试案例,指望在该测试案例程序抛出异常,并检验其是否抛出异常。

简单示例:

/// <summary>
/// 计算从from到to的全部整数的和
/// </summary>
public int Sum(int from, int to)
{
    if (from > to)
    {
        throw new ArgumentException("参数from必须小于to");
    }
    int sum = 0;
    for (int i = from; i <= to; i++)
    {
        sum += i;
    }
    return sum;
}

在程序中,如果参数from >to则抛出异常new ArgumentException("参数from必须小于to");

为了检验该程序在该条件下是否真的会抛出异常,能够创造测试案例from=100 > to=50
指望Sum()函数代码中执行:throw new ArgumentException("参数from必须小于to");,因此咱们要测试指望抛出的异常ArgumentException

使用标签[ExpectedException(typeof(“抛出的异常对象”))]

单元测试代码:

//异常测试,添加ExpectedException
[TestMethod]
[ExpectedException(type(ArgumentException))]
public void SumTest_ArgumentException_TrowException()
{
    Calculator bjCalcultor = new Calculator();
    int from=100,to=50;
    calc.Sum(from, to);
}

由于程序抛出了咱们指望的异常,因此该测试经过。如若程序没有抛出该异常则测试失败。

异常测试




7.单元测试中针对状态的间接测试

  • 基于状态的测试(也称状态验证),是指在方法执行以后,经过检查被测系统及其协做者(依赖项)的状态来检测该方法是否正确工做

  • 简单示例:

    下面的方法isLastFilenameValid(string filename)在运行后会改变类中属性wasLastFileNameValid的值

//用于存储状态的结果用于之后的验证
public bool wasLastFileNameValid { get; set; }
//判断输入的字符串是不是.txt文件名
public bool isLastFilenameValid(string filename)
{
   if (!(filename .ToLower()).EndsWith("txt"))
   {
       wasLastFileNameValid = false;
       return false ;
   }
   else
   {
       wasLastFileNameValid = true;
       return true;
   }
}
  • 单元测试函数:

    该测试是测试isLastFilenameValid(),

    由于该函数是把结果赋值给类中属性wasLastFileNameValid,

    因此此处验证的是Calculator类中属性wasLastFileNameValid是否符合咱们的指望,

    而不是简单的验证isLastFilenameValid()的返回值是否符合咱们的指望。

[TestMethod()]
public void isLastFilenameValid_ValidName_ReturnTrue()
{
    Calculator calc = new Calculator();
    string fileName = "test.txt";
    calc.isLastFilenameValid(fileName)
    Assert.IsTrue(calc.wasLastFileNameValid);
  
}




8.单元测试在MVC模式中的实现

参考

  • 由于MVC模式中的Controller类中的Action的返回值是和普通类的方法不同的,

    Action的返回值是ActionResult类型的,其子类又有许多,

    具体怎么实现对MVC模式的单元测试呢?请看一个简单的示例:

    代码背景:在一个MVC项目中的HomeController控制器中有一个Action是Index()

    首先先定义一个Person类其中有Id和Name两个属性

    Action以下:

    public class HomeController : Controller
      {
          // GET: Home
          public ActionResult Index()
          {
              return View("Index",new Person { Id = 001, Name = "shanzm" });
          }
      }

    对上面的HomeController中的Index()进行一个简单的单元测试

    新建一个单元测试项目(或者在建立MVC项目的时候选中单元测试的按钮,则自动生成一个单元测试项目)

    注意必定要先安装MVC的程序集,NuGet:Install-Package Microsoft.AspNet.Mvc -Version 5.2.3

    [TestMethod()]
      public void Index_Index_ReturnTrue()
      {
          //Arrage:准备测试对象
          HomeController hcont = new HomeController();
          //Act:执行测试函数
          ViewResult  result =(ViewResult)hcont.Index();
          var viewName = result.ViewName;
          Person model = (Person)result.Model;
          //Assert:断言符合指望
          Assert.IsTrue(viewName == "Index" && model.Id == 001 && model.Name == "shanzm"&& );
      }

说明:

  1. 若是View()函数没指定视图,而是使用默认的视图,则视图名为空,因此若是名称不写的时候咱们能够断言ViewName是空。

  2. 注意在Action中的ViewBag传递的数据在单元测试中须要经过ViewData方式获取(由于ViewBag是对ViewData的动态封装,在同一个Action中两者数据相通,此乃ASP.NET MVC的基础,不详述)

  3. 其实呀,MVC模式做为UI层,有许多东西实际上是很难(但不是不能够)模拟对象去进行单元测试的,通常其实不推荐作过多的单元测试。(注意不是不作,是不作过多过复杂的单元测试)




8.单元测试相关参考

书籍:.NET 单元测试的艺术

书籍:单元测试之道C#版

微软:dotnet文档

博客园:对比MS Test与NUnit Test框架

博客园:.net持续集成测试篇之Nunit文件断言、字符串断言及集合断言

博客园:.netcore持续集成测试篇之MVC层单元测试




9.示例源代码下载

示例源代码下载

相关文章
相关标签/搜索