• Nunit单元测试


    NUnit是.net平台上使用得最为广泛的测试框架之一,本文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试的初学者参考。

    继续下文之前,先来看看一个非常简单的测试用例(TestCase):

     [Test]
     public void AdditionTest()
     {
         int expectedResult = 2;
     
         Assert.AreEqual(exptectedResult, 1 + 1);
    }

    你肯定会说这个TestCase也太白痴了吧!这也是许多NUnit文档被人诟病的一点,但是我的理解并不是这样,xUnit本来就是编写UT的简易框 架,keep it simple and stupid,任何通过复杂的TestCase来介绍NUnit的用法都是一种误导,UT复杂之处在于如何在实际项目中应用和实施,而不是徘徊于该如何使 用NUnit。

    主要内容:
    1、NUnit的基本用法
    2、测试用例的组织
    3、NUnit的断言(Assert)
    4、常用单元测试工具介绍

    一、NUnit的基本用法 和 其他xNUnit框架不同的是,NUnit框架使用Attribute(如前面代码中的[Test])来描述测试用例的,也就是说我们只要掌握了Attribute的用法,也就基本学会如何使用NUnit了。VSTS所集成的单元测试也支持类似NUnit的Attributes,下表对比了NUnit和VSTS的标记:

    usage

    NUnit            attributes

    VSTS            attributes

    标识测试类

    TestFixture

    TestClass

    标识测试用例(TestCase)

    Test

    TestMethod

    标识测试类初始化函数

    TestFixtureSetup

    ClassInitialize

    标识测试类资源释放函数

    TestFixtureTearDown

    ClassCleanup

    标识测试用例初始化函数

    Setup

    TestInitialize

    标识测试用例资源释放函数

    TearDown

    TestCleanUp

    标识测试用例说明

    N/A

    Description

    标识忽略该测试用例

    Ignore

    Ignore

    标识该用例所期望抛出的异常

    ExpectedException

    ExpectedException

    标识测试用例是否需要显式执行

    Explicit

    ?

    标识测试用例的分类

    Category

    ?

     现在,让我们找一个场景,通过示例来了解上述NUnit标记的用法。来看看一个存储在数据库中的数字类:

    这是我们常见的DAL+Entity的设计,DigitDataProvider和Digit类的实现代码如下:

    1)Digit.cs类:

    using System;
    using System.Data;
     
     namespace Product
     {
         /// <summary>
         /// Digit 的摘要说明
         /// </summary>
         /// 创 建 人: 罗旭成
         /// 创建日期: 2013-10-22
         /// 修 改 人: 
         /// 修改日期:
         /// 修改内容:
         /// 版    本:
         public class Digit
         {
             private Guid _digitId;
             public Guid DigitID
             {
                 get { return this._digitId; }
                 set { this._digitId = value; }
             }
    
             private int _value = 0;
             public int Value
             {
                 get { return this._value; }
                 set { this._value = value; }
             }
     
             #region 构造函数
             /// <summary>
             /// 默认无参构造函数
             /// </summary>
             /// 创 建 人: 罗旭成
             /// 创建日期: 2013-10-22
             /// 修 改 人: 
             /// 修改日期:
             /// 修改内容:
             public Digit()
             {
                 //
                 // TODO: 在此处添加构造函数逻辑
                 //
             }
     
             /// <summary>
             /// construct the digit object from a datarow
             /// </summary>
             /// <param name="row"></param>
             public Digit(DataRow row)
             {
                 if (row == null)
                 {
                     throw new ArgumentNullException();
                 }
     
                 if (row["DigitID"] != DBNull.Value)
                 {
                     this._digitId = new Guid(row["DigitID"].ToString());
                 }
     
                 if (row["Value"] != DBNull.Value)
                 {
                     this._value = Convert.ToInt32(row["Value"]);
                 }
             }
            
             #endregion
         }
     }

    2)DigitDataProvider类:

     using System;
     using System.Data;
     using System.Data.SqlClient;
     using System.Collections;
       
     namespace Product
     {
           /// <summary>
           /// DigitDataProvider 的摘要说明
          /// </summary>
          /// 创 建 人: 罗旭成
          /// 创建日期: 2013-10-22
          /// 修 改 人: 
          /// 修改日期:
          /// 修改内容:
          /// 版    本:
          public class DigitDataProvider
          {
              /// <summary>
              /// 定义数据库连接
              /// </summary>
              private SqlConnection _dbConn;
              public SqlConnection Connection
              {
                  get { return this._dbConn; }
                  set { this._dbConn = value; }
              }
              
              #region 构造函数
              /// <summary>
              /// 默认无参构造函数
              /// </summary>
              /// 创 建 人: 罗旭成
              /// 创建日期: 2013-10-22
              /// 修 改 人: 
              /// 修改日期:
              /// 修改内容:
              public DigitDataProvider()
              {
                  //
                  // TODO: 在此处添加构造函数逻辑
                  //
              }
     
              public DigitDataProvider(SqlConnection conn)
              {
                  this._dbConn = conn;
              }
              
              #endregion
              
              #region 成员函数定义
      
              /// <summary>
              /// retrieve all Digits in the database
              /// </summary>
              /// <returns></returns>
              public ArrayList GetAllDigits()
              {
                  // retrieve all digit record in database
                  SqlCommand command = this._dbConn.CreateCommand();
                  command.CommandText = "SELECT * FROM digits";
                  SqlDataAdapter adapter = new SqlDataAdapter(command);
                  DataSet results = new DataSet();
                  adapter.Fill(results);
      
                  // convert rows to digits collection
                  ArrayList digits = null;
      
                  if (results != null && results.Tables.Count > 0)
                  {
                      DataTable table = results.Tables[0];
                      digits = new ArrayList(table.Rows.Count);
      
                      foreach (DataRow row in table.Rows)
                      {
                          digits.Add(new Digit(row));
                      }
                  }
      
                  return digits;
              }
      
              /// <summary>
              /// remove all digits from the database
              /// </summary>
              /// <returns></returns>
              public int RemoveAllDigits()
             {
                  // retrieve all digit record in database
                  SqlCommand command = this._dbConn.CreateCommand();
                  command.CommandText = "DELETE FROM digits";
     
                 return command.ExecuteNonQuery();
             }
     
              /// <summary>
             /// retrieve and return the entity of given value
             /// </summary>
             /// <exception cref="System.NullReferenceException">entity not exist in the database</exception>
             /// <param name="value"></param>
             /// <returns></returns>
             public Digit GetDigit(int value)
             {
                 // retrieve entity of given value
                 SqlCommand command = this._dbConn.CreateCommand();
                 command.CommandText = "SELECT * FROM digits WHERE Value='" + value + "'";
                 SqlDataAdapter adapter = new SqlDataAdapter(command);
                 DataSet results = new DataSet();
                 adapter.Fill(results);
     
                 // convert rows to digits collection
                 Digit digit = null;
     
                 if (results != null && results.Tables.Count > 0
                     && results.Tables[0].Rows.Count > 0)
                 {
                     digit = new Digit(results.Tables[0].Rows[0]);
                 }
                 else
                 {
                     throw new NullReferenceException("not exists entity of given value");
                 }
     
                 return digit;
             }
     
             /// <summary>
             /// remove prime digits from database
             /// </summary>
             /// <returns></returns>
             public int RemovePrimeDigits()
             {
                 throw new NotImplementedException();
             }
     
             #endregion
         }
     }
     

    3)新建测试数据库:

    CREATE TABLE [dbo].[digits] (
        [DigitID] [uniqueidentifier] NOT NULL ,
        [Value] [int] NOT NULL 
    ) ON [PRIMARY]
    GO

    下面,我们开始尝试为DigitDataProvider类编写UT,新建DigitDataProviderTest.cs类。
    1、添加nunit.framework引用

    并在DigitDataProviderTest.cs中添加:

    using NUnit.Framework;

    2、编写测试用例
    1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。

     [TestFixture]
     public class DigitProviderTest
     {
         public DigitProviderTest()
         {
         }
     }

    2)编写DigitDataProvider.GetAllDigits()的测试函数

     /// <summary>
     /// regular test of DigitDataProvider.GetAllDigits()
     /// </summary>
     [Test]
     public void TestGetAllDigits()
     {
         // initialize connection to the database
         // note: change connection string to ur env
         IDbConnection conn = new SqlConnection(
             "Data source=localhost;user id=sa;password=sa;database=utdemo");
         conn.Open();
     
         // preparing test data
         IDbCommand command = conn.CreateCommand();
         string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";
     
         for (int i = 1; i <= 100; i++)
         {
             command.CommandText = string.Format(
                 commadTextFormat, Guid.NewGuid().ToString(), i.ToString());
             command.ExecuteNonQuery();
         }
     
         // test DigitDataProvider.GetAllDigits()
         int expectedCount = 100;
         DigitDataProvider provider = new DigitDataProvider(conn as SqlConnection);
         IList results = provider.GetAllDigits();
     
         // that works?
         Assert.IsNotNull(results);
         Assert.AreEqual(expectedCount, results.Count);
     
         // delete test data
         command = conn.CreateCommand();
         command.CommandText = "DELETE FROM digits";
         command.ExecuteNonQuery();
     
         // close connection to the database
         conn.Close();
     }

    什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:

     [Test]
     public void TestCase()
     {
         // 1) initialize test environement, like database connection
         
     
         // 2) prepare test data, if neccessary
         
     
         // 3) test the production code by using assertion or Mocks.
         
     
         // 4) clear test data
         
     
         // 5) reset the environment
         
     }

    NUnit要求每一个测试函数都可以独立运行(往往有人会误解NUnit并按照Consoler中的排序来执行),这就要求我们在调用目标函数之前先要初 始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如 删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,让我们对上面的代码进行一次Refactoring,Extract Method:

     /// <summary>
     /// connection to database
     /// </summary>
     private static IDbConnection _conn;
     
     /// <summary>
     /// 初始化测试类所需资源
     /// </summary>
     [TestFixtureSetUp]
     public void ClassInitialize()
     {
         // note: change connection string to ur env
         DigitProviderTest._conn = new SqlConnection(
             "Data source=localhost;user id=sa;password=sa;database=utdemo");
         DigitProviderTest._conn.Open();
     }
     
     /// <summary>
     /// 释放测试类所占用资源
     /// </summary>
     [TestFixtureTearDown]
     public void ClassCleanUp()
     {
         DigitProviderTest._conn.Close();
     }
     
     /// <summary>
     /// 初始化测试函数所需资源
     /// </summary>
     [SetUp]
     public void TestInitialize()
     {
         // add some test data
         IDbCommand command = DigitProviderTest._conn.CreateCommand();
        string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";
     
         for (int i = 1; i <= 100; i++)
         {
             command.CommandText = string.Format(
                 commadTextFormat, Guid.NewGuid().ToString(), i.ToString());
             command.ExecuteNonQuery();
         }
     }
     
     /// <summary>
     /// 释放测试函数所需资源
     /// </summary>
     [TearDown]
     public void TestCleanUp()
     {
         // delete all test data
         IDbCommand command = DigitProviderTest._conn.CreateCommand();
         command.CommandText = "DELETE FROM digits";
     
         command.ExecuteNonQuery();
     }
     
     /// <summary>
     /// regular test of DigitDataProvider.GetAllDigits()
     /// </summary>
     [Test]
     public void TestGetAllDigits()
     {
         int expectedCount = 100;
         DigitDataProvider provider = 
             new DigitDataProvider(DigitProviderTest._conn as SqlConnection);
     
         IList results = provider.GetAllDigits();
         // that works?
         Assert.IsNotNull(results);
         Assert.AreEqual(expectedCount, results.Count);
     }

    NUnit提供了以下Attribute来支持测试函数的初始化:
    TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
    TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
    Setup:在当前测试类的每一个测试函数运行前调用;
    TearDown:在当前测试类的每一个测试函数运行后调用。

    3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
    唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。

    /// <summary>
     /// regular test of DigitDataProvider.RemovePrimeDigits
     /// </summary>
     [Test, Ignore("Not Implemented")]
     public void TestRemovePrimeDigits()
     {
         DigitDataProvider provider = 
             new DigitDataProvider(DigitProviderTest._conn as SqlConnection);
     
         provider.RemovePrimeDigits();
     }

    Ignore的用法:

    Ignore(string reason)

    4)编写DigitDataProvider.GetDigit()的测试函数
    当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?

     /// <summary>
     /// Exception test of DigitDataProvider.GetDigit()
     /// </summary>
     [Test, ExpectedException(typeof(NullReferenceException))]
     public void TestGetDigit()
     {
         int expectedValue = 999;
         DigitDataProvider provider = 
             new DigitDataProvider(DigitProviderTest._conn as SqlConnection);
     
         Digit digit = provider.GetDigit(expectedValue);
     }

    ExpectedException的用法

    ExpectedException(Type t)
    ExpectedException(Type t, string expectedMessage)

    在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。

    二、测试函数的组织
    现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:

     [Test, Explicit]
     public void OneHourTest()
     {
         //
     }

    不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:

     [Test, Explicit, Category("LongTest")]
     public void OneHourTest()
     {
         ...
     }
     
     [Test, Explicit, Category("LongTest")]
     public void TwoHoursTest()
     {
         ...
     }

    这样,只有当显示选中LongTest分类时,这些TestCase才会执行

    三、NUnit的断言
    NUnit提供了一个断言类NUnit.Framework.Assert,可用来进行简单的state base test(见idior的Enterprise Test Driven Develop),可别对这个断言类期望太高,在实际使用中,我们往往需要自己编写一些高级断言。
    常用的NUnit断言有:

    method

    usage

    example

    Assert.AreEqual(object            expected, object actual[, string message])

    验证两个对象是否相等

    Assert.AreEqual(2, 1+1)

    Assert.AreSame(object            expected, object actual[, string message])

    验证两个引用是否指向同意对象

    object expected = new object();

    object actual = expected;

    Assert.AreSame(expected, actual)

    Assert.IsFalse(bool)

    验证bool值是否为false

    Assert.IsFalse(false)

    Assert.IsTrue(bool)

    验证bool值是否为true

    Assert.IsTrue(true)

    Assert.IsNotNull(object)

    验证对象是否不为null

    Assert.IsNotNull(new object())

    Assert.IsNull(object)

    验证对象是否为null

    Assert.IsNull(null);

    这 里要特殊指出的Assert.AreEqual只能处理基本数据类型和实现了Object.Equals接口的对象的比较,对于我们自定义对象的比较,通 常需要自己编写高级断言,这个问题郁闷了我好一会,下面给出一个用于level=1的情况下的对象比较的高级断言的实现:

     public class AdvanceAssert
     {
         /// <summary>
         /// 验证两个对象的属性值是否相等
         /// </summary>
         /// <remarks>
         /// 目前只支持的属性深度为1层
         /// </remarks>
         public static void AreObjectsEqual(object expected, object actual)
         {
             // 若为相同引用,则通过验证
             if (expected == actual)
             {
                 return;
             }
     
             // 判断类型是否相同
             Assert.AreEqual(expected.GetType(), actual.GetType());
     
             // 测试属性是否相等
             Type t = expected.GetType();
             PropertyInfo[] properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
     
             foreach (PropertyInfo property in properties)
             {
                 object obj1 = t.InvokeMember(property.Name, 
                     BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, 
                     null, expected, null);
                 object obj2 = t.InvokeMember(property.Name, 
                     BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, 
                     null, actual, null);
     
                 // 判断属性是否相等
                 AdvanceAssert.AreEqual(obj1, obj2, "assertion failed on " + property.Name);
             }
         }
     
         /// <summary>
         /// 验证对象是否相等
         /// </summary>
         private static void AreEqual(object expected, object actual, string message)
         {
             Type t = expected.GetType();
     
             if (t.Equals(typeof(System.DateTime)))
             {
                 Assert.AreEqual(expected.ToString(), actual.ToString(), message);
             }
             else
             {
                 // 默认使用NUnit的断言
                 Assert.AreEqual(expected, actual, message);
             }
         }
     }

    四、常用单元测试工具介绍:
    1、NUnit:目前最高版本为2.6.2(也是本文所使用的NUnit的版本)
    下载地址:http://www.nunit.org

    2、TestDriven.Net:一款把NUnit和VS IDE集成的插件
    下载地址:http://www.testdriven.net/

    3、NUnit2Report:和nant结合生成单元测试报告
    下载地址:http://nunit2report.sourceforge.net

    4、Rhino Mocks 2:个人认为时.net框架下最好的mocks库,而且支持.net 2.0, rocks~!
    下载地址:http://www.ayende.com/projects/rhino-mocks.aspx


    想不到一口气写了这么多,前段时间在公司的项目中进行了一次单元测试的尝试,感触很深,看了idior的文章后更加觉得单元测试日后会成为项目的必需部分。在后续的文章中,我将讨论mocks,自定义测试框架和自动化测试工具,希望能和园子里的uter多多讨论。

    好向往TDD~~

  • 相关阅读:
    sql server 数据库可疑处理方法。
    执行gpedit.msc 提示找不到程序
    uni-app Post springboot 后台接收数据为null 解决办法
    组件接口升级商品编码文件
    mysql 拼接字符
    mysql 截取前7位
    为什么具有编程思维的孩子更容易成功?孩子为什么要学编程?你想要的答案都在这儿!
    利用支持MicroPython的TPYBoard开发板自制PM2.5检测仪(萝卜教育学科式编程)
    Micropython教程实例之USB-HID应用(萝卜学科编程)
    教程Micropython自制小型家庭气象站(萝卜教育)
  • 原文地址:https://www.cnblogs.com/jara/p/3382774.html
Copyright © 2020-2023  润新知