風語·深蓝

Agile Methodology, HeadStorm And MindMap, they will change me.

导航

单元测试需要有一定的工具和框架的支撑,在早期,一般我们使用的都是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并不能使用在单元测试中,而是在集成测试中使用


以下附上一段典型的单元测试代码作为展示

View Code
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);
}
}
}