关于C#程序的单元测试

时间:2020-01-23 12:34:38   收藏:0   阅读:90
志铭-2020年1月23日 11:49:41

1.单元测试概念




2.单元测试的原则

根本原则:

其他一些规范:




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("测试类别")] 给测试自定义分类,便于有选择的运行指定类别的单元测试

说明:


[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.单元测试中针对状态的间接测试

//用于存储状态的结果用于以后的验证
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;
   }
} 
[TestMethod()]
public void isLastFilenameValid_ValidName_ReturnTrue()
{
    Calculator calc = new Calculator();
    string fileName = "test.txt";
    calc.isLastFilenameValid(fileName)
    Assert.IsTrue(calc.wasLastFileNameValid);
  
}




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

参考

说明:

  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.示例源代码下载

示例源代码下载

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!