单元测试需要有一定的工具和框架的支撑,在早期,一般我们使用的都是NUnit这套单元测试框架进行。后来微软在Visual Studio中集成了单元测试功能后,提供了更为强劲的功能以及集成整合能力,就没有必要再继续使用Nunit了。
这一章节,主要就是介绍Visual Stuido中常见的单元测试相关的Attribute的功能和使用场景。
基本类Attribute
TestClassAttribute
用于标识包含测试方法的类。任何一个单元测试类,必须在类上添加该Attribute,否则不会被Visual Studio识别为单元测试类,里面的所有方法也无法正确识别;
TestMethodAttribute
用于标记测试方法。只有添加了该Attribute的方法才会被识别为单元测试方法,才会被添加到Test List中;
PriorityAttribute
用于标记单元测试方法的执行优先级,即先后顺序。(但我测试时候未能生效,别的朋友有成功过,具体原因不详)
DescriptionAttribute
标记并描述当前单元测试方法,会出现在Test List中(默认不显示这列,需要手动添加),方便在运行测试时,方便的了解测试方法的含义
TestPropertyAttribute
传递Key/Value元数据的一个Attribute,本身没什么特别意义,允许在一个方法上标记多个。
初始化与清理类Attribute
此类别的所有属性都必须写在静态方法上
AssemblyInitializeAttribute
当一个程序集中第一个单元测试方法被执行前会执行该方法,用于初始化操作
ClassInitializeAttribute
当第一次执行当前类中的单元测试方法前会执行该方法,用于初始化操作
TestInitializeAttribute
当前类中的每个单元测试方法都会先执行该方法
TestCleanupAttribute
当前类中每个单元测试方法后,都会执行该方法,用于卸载和清除
ClassCleanupAttribute
当执行完该类所有单元测试之后,执行该方法
AssemblyCleanup
当当前程序集中所有单元测试都执行完后,执行该方法
异常以及超时类Attribute
ExpectedExceptionAttribute
标记当前单元测试应该出现的异常,如果应该出现的指定类型的异常没有出现,则认为这个单元测试失败
TimeoutAttribute
设定当前单元测试的最大执行时间,如果该方法执行的时间超过了该值,则会认为该单元测试未能通过
特殊作用类Attribute
DataSourceAttribute
逐行读取指定数据源中的数据,每行数据会执行当前单元测试方法一次。通过testContextInstance对象上的DataRow索引器属性访问每行上的各列数据。
需要注意的是,以上绝大多数Attribute并不能使用在单元测试中,而是在集成测试中使用
以下附上一段典型的单元测试代码作为展示
using System;
using System.Data;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using ALite.Core.UnitTestSimple;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace ALite.Core.Tests
{
[TestClass]
public class OrderUnitTest
{
private TestContext _testContextInstance;
public TestContext TestContext
{
get { return _testContextInstance; }
set { _testContextInstance = value; }
}
[TestMethod]
[Description("A normal unit testing for the method 'Submit.'")]
public void TestNormalSubmit()
{
// Mock
var mockPersistence = new Mock<IPersistence>();
mockPersistence.Setup(e => e.Save(It.IsAny<Order>())).Returns(true);
// Arrange
var target = new Order
{
PesistenceHandler = mockPersistence.Object
};
const bool expected = true;
// Action
bool result = target.Submit();
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
// 是且只有是抛出DataException类型的异常时才会算通过,即便是子类也会判断为未通过
[ExpectedException(typeof(DataException))]
[Description("A unit testing for the method 'Submit' with a DataException when using the property PersistenceHandler")]
public void TestSubmitWithException()
{
// Mock
var mockPersistence = new Mock<IPersistence>();
mockPersistence.Setup(e => e.Save(It.IsAny<Order>())).Throws(new DataException());
// Arrange
var target = new Order
{
PesistenceHandler = mockPersistence.Object
};
const bool expected = true;
// Action
bool result = target.Submit();
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
[Description("举例说明如何测试私有方法和使用TestProperty属性")]
[TestProperty("City", "深圳")]
[TestProperty("Province", "广东")]
[TestCategory("Category1")]
public void TestCountTotalPrice()
{
var method = Util.GetCallingMethod(false, 0);
var attributeType = typeof(TestPropertyAttribute);
var attributes = method.GetCustomAttributes(attributeType, true);
var city = ((TestPropertyAttribute)attributes[0]).Value;
var province = ((TestPropertyAttribute)attributes[1]).Value;
var address = string.Format("{0} {1}", province, city);
// Mock
var mockDeliveryManageHandler = new Mock<IDeliveryManage>();
mockDeliveryManageHandler.Setup(e => e.CountDeliveryFee(It.IsAny<string>())).Returns(10.00m);
// Arrange
var orderDetails1 = new OrderDetail()
{
TotalPrice = 10.00m
};
var orderDetails2 = new OrderDetail()
{
TotalPrice = 20.00m
};
var orderDetails3 = new OrderDetail()
{
TotalPrice = 30.00m
};
var details = new List<OrderDetail> {orderDetails1, orderDetails2, orderDetails3};
var target = new Order()
{
Destination = address,
DeliveryManageHandler = mockDeliveryManageHandler.Object,
OrderDetails = details
};
const decimal expected = 70m;
// Action
// 通过反射测试私有方法,比IDE提供的方式会更灵活一些
var typeTarget = typeof (Order);
var methodTarget = typeTarget.GetMethod("CountTotalPrice", BindingFlags.Instance | BindingFlags.NonPublic);
var objResult = methodTarget.Invoke(target, null);
// Assert
Assert.AreEqual(expected, (decimal)objResult);
}
[DeploymentItem("1.1-ALite.Core.Tests\\1.xml"), TestMethod]
[DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\1.xml", "Address", DataAccessMethod.Sequential)]
[Description("举例说明如何使用DataSourceAttribute属性, 该单元测试会读取数据源中的每一行数据进行测试")]
public void TestCountTotalPriceWithVarious()
{
var province = this._testContextInstance.DataRow["Province"];
var city = this._testContextInstance.DataRow["City"];
var fee = Convert.ToDecimal(this._testContextInstance.DataRow["Fee"]);
var address = string.Format("{0} {1}", province, city);
// Mock
var mockDeliveryManageHandler = new Mock<IDeliveryManage>();
mockDeliveryManageHandler.Setup(e => e.CountDeliveryFee(It.IsAny<string>())).Returns(10.00m);
// Arrange
var orderDetails1 = new OrderDetail()
{
TotalPrice = 10.00m
};
var orderDetails2 = new OrderDetail()
{
TotalPrice = 20.00m
};
var orderDetails3 = new OrderDetail()
{
TotalPrice = 30.00m
};
var details = new List<OrderDetail> { orderDetails1, orderDetails2, orderDetails3 };
var target = new Order()
{
Destination = address,
DeliveryManageHandler = mockDeliveryManageHandler.Object,
OrderDetails = details
};
decimal expected = fee;
// Action
// 通过反射测试私有方法,比IDE提供的方式会更灵活一些
var typeTarget = typeof(Order);
var methodTarget = typeTarget.GetMethod("CountTotalPrice", BindingFlags.Instance | BindingFlags.NonPublic);
var objResult = methodTarget.Invoke(target, null);
// Assert
Assert.AreEqual(expected, (decimal)objResult);
}
}
}