前面的笔记做了关于Ninject(MVC三类工具里面第一类IoC容器),本次的笔记是关于VS里面提供的Unit Testing工具的使用以及Moq(模拟工具)。
1.Visual Studio自带的单元测试工具
除了使用微软自带的单元测试工具,我们还可以选择NUnit--非常流行的一款测试工具。接下来我们创建一个项目ProductApp,你也可以使用NUnit,猛击这里获取。它的使用跟VS自带的非常类似。
首先我们创建用来的测试的类和接口,如下所示:
public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } public interface IProductRepository{ IEnumerable<Product> GetProducts(); void UpdateProduct(Product product); } public interface IPriceReducer { void ReducePrices(decimal priceReduction); }
这里的Product类跟前面笔记里面的是一样的,IProductRepository接口定义两个方法:分别用来获取和更新。这个符合Repository模式,我们在前面的笔记里面介绍的。IPriceReducer定义了一个具体的降价方法,针对所有的Products。我们的目的就是要实现这个接口并满足下面的条件:
a.所有的Products都要降价
b.总的降价必须等于所有的Products数量与降价数额的乘积
c.Repository的UpdateProduct方法应该能被每一个product调用
d.降价后所有的Products的价格不能低于1
接下来我们添加一个实现IProductRepository的类,如下所示:
public class FakeRepository : IProductRepository { private Product[] products = { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; public IEnumerable<Product> GetProducts() { return products; } public void UpdateProduct(Product productParam) { foreach(Product p in products .Where(e => e.Name == productParam.Name) .Select(e => e)) { p.Price = productParam.Price; } UpdateProductCallCount++; } public int UpdateProductCallCount { get; set; } public decimal GetTotalValue() { return products.Sum(e => e.Price); } }
如果你跟我一样对3.0里面出现的查询表达式LINQ不熟悉的话,我建议你花时间看下相关的资料,前面5天我在补习这方面的知识,为了方便学习MVC,而且了解了LINQ以后你会慢慢喜欢LINQ的。下面接着是实现IPriceReducer接口,如下所示:
public class MyPriceReducer : IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { //这里先不去实现 throw new NotImplementedException(); } }
这里我们又可以看到DI,而且是通过构造器参数来实现依赖注入的(如果你比较对DI比较生疏,可以看看我前面做的笔记)。
下面我们开始进入测试的正题了,创建一个测试项目。方法很简单,下图是一种:
在我们刚才的那个没有具体实现的方法上右键创建单元测试即可。接下来的操作如图:
单元测试是在一个单独的项目里面,这里我们没有创建所以会弹出创建该测试项目的对话框,我们给它取名为:ProductApp.Tests.创建完成后会自动创建一个MyPriceReducerTest.cs类。里面默认会生成一些方法,我们更改如下:
namespace ProductApp.Tests { [TestClass] public class MyPriceReducerTest { //测试是否是所有的价格都会发生改变 [TestMethod] public void All_Prices_Are_Changed) { // Arrange FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; IEnumerable<decimal> prices = repo.GetProducts().Select(e => e.Price); decimal[] initialPrices = prices.ToArray(); MyPriceReducer target = new MyPriceReducer(repo); // Act target.ReducePrices(reductionAmount); //合并两个序列(4.0里面新提供的扩展方法) //这里是将开始的价格跟降价后的价格逐一对比,如果有相等的说明降价失败。 prices.Zip(initialPrices, (p1, p2) => { if (p1 == p2) { Assert.Fail(); } return p1; }); } } }
这里我们可以发现测试方法上面的会标记一些特性(Attributes),如[TestMethod],[TestClass]。如果测试的项目里面的类没有包括这些特性的类和方法是不会测试的,VS会忽略它们。我们可以发现是按照arrange/act/assert的模式来进行单元测试的。arrange:初始化测试的环境属于准备阶段;act:执行测试;assert:断言,测试的结果。对于测试方法的约定有很多种,我们这里的是简单的,通过方法名就能够知道测试的内容是什么。其实如果你不喜欢大可以选择自己的方式或者是团队的约定,主要是为了开发人员能够理解。
关于创建单元测试有很多不同的方式,一种通常的方式就是在一个方法里面测试某一功能的所有情况。这里我们倾向将很多不同的情况分散到一个一个小的测试方法里面来进行。这种通过测试不断来完善我们的代码的开发模式成为TDD(测试驱动开发).接着创建测试方法,代码如下:
[TestMethod] public void Correct_Total_Reduction_Amount() { // Arrange FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; decimal initialTotal = repo.GetTotalValue(); MyPriceReducer target = new MyPriceReducer(repo); // Act target.ReducePrices(reductionAmount); // Assert Assert.AreEqual(repo.GetTotalValue(),(initialTotal - (repo.GetProducts().Count() * reductionAmount)));} [TestMethod] public void No_Price_Less_Than_One_Dollar() { // Arrange FakeRepository repo = new FakeRepository(); decimal reductionAmount = decimal.MaxValue; MyPriceReducer target = new MyPriceReducer(repo); // Act target.ReducePrices(reductionAmount); // Assert foreach (Product prod in repo.GetProducts()) { Assert.IsTrue(prod.Price >= 1); } }
上面的两个测试方法应该好理解吧。这里同样实现了依赖注入,而且是构造器注入。关于Assert静态方法还有很多,我们可以猛击这里了解。每一个静态方法让我们针对单元测试的一个方面来测试,如果Assert失败就会抛出一个异常,这也就意味着整个单元测试失败。每一个单元测试是独立的,其他的单元测试不会因为某一个单元测试的失败而停止。
Assert的每一个静态方法都有一个重载的带有string参数的方法,这个参数包含了Assert失败时的异常信息。值得注意的是这个ExceptionExpected的特性,这是一个断言,只有当单元测试通过异常类型的参数抛出一个具体化的类型的异常才成功。这是一种非常灵巧的方式,它确保了抛出异常不需要在我们的测试代码里面混入try...catch.
接下来我们运行测试,这时会报错,因为我们有个方式还没实现呢?接着实现该方法,代码如下:
public class MyPriceReducer : IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { foreach (Product p in repository.GetProducts()) { p.Price = Math.Max(p.Price - priceReduction, 1); //既能降价而且可以保证每个Product的价格不低于1元 repository.UpdateProduct(p); } } }
再一次请大家注意这里的加黑部分实现了构造器注入的。
好了,本次的笔记就到这里。今天的主要介绍了VS自带的测试工具,比较容易理解。本来没打算记这部分的笔记,但为了保持整个笔记的连贯,还是写了。如果你已经对测试比较熟悉了,抱歉浪费你时间了。后面还有一个Moq工具的介绍,之后会有一个小的应用:SportsStore.
笔记中肯定会有我理解不准确或错误的地方,还请路过的朋友多多指导帮助,谢谢!
晚安!