使用Moq
前面我创建了一个FakeRepository类来支持我们的测试,但是并没有创建一个实际的Repository的实现,所以我需要一个替代品。FakeRepository类是IProductRepository接口的一个模拟实现,Moq是一个框架,为了让我们能够快速,简便的实现模拟,而不用手工添加一些额外的代码。
首先我们需要下载一个Moq的组件,猛击这里可以下载。在我们的测试项目ProductApp.Tests里面添加对Moq的引用。使用mocking工具好处就是我们能够创建足够适应我们功能的mocks来帮助测试。可能在这个项目里面我们体会不出Moq的优势,但是在实际的项目中,Moq让我们能够容易的达到需要自身测试的mock实现阶段,因为Moq包含了丰富的代码帮我们实现。我们能够创建很多小的手工的mocks来让它发挥作用,我们也能够将这些代码移动到一个基类里面重复使用。但是这样也会再次变得复杂。当测试能够更加专注某一个点,更加小的的时候会使测试运行得更好。所以我们将每个测试点分解的越简单越好。
使用Moq创建一个Mock需要两个步骤,
首先我们需要创建一个新的Mock<T>,如这样:Mock<IProductRepository> mock = new Mock<IProductRepository>();
其次,设置我们的实现需要阐释的行为(behavior)。Moq会自动实现所有的方法和我们给定类型的属性,但是使用的是类型的默认值。
比如,IProductRepository.GetProducts()方法,返回一个空的IEnumerable<Product>。为了改变Moq实现一个类型成员的方式,我们必须使用Setup()方法。下面这样:
Product[] products = new Product[]
{
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}
};
mock.Setup(m => m.GetProducts()).Returns(products);
当我们创建一个新的Moq的行为(behavior)的时候需要考虑三个因素:
第一,方法的选择.Moq使用LINQ和Lambda表达式,当我们调用一个Setup方法时,Moq会传递我们让它实现的接口,这些都是巧妙的包裹在了LINQ里面不需要我们去深入,但是Moq允许我们通过一个Lambda表达式来选择要配置和验证的方法。当我们要为GetProducts()定义一个行为的时候我们可以这样做:mock.Setup(m => m.GetProducts()).(其他的方法);我们不必去管它是如果工作的,只需要知道相应的使用就可以了。这里的GetProducts()方法没有参数,处理会更加简单。
当有参数的时候,我需要考虑两个因素:第一,参数过滤器;我们可以让Moq根据传入不同的参数做出不同的响应。为了演示,这里增加了一个接口:
public interface IMyInterface
{
string ProcessMessage(string message);
}
然后就可以根据传入不同的参数来创建该接口的Mock实现。如下所示:
Mock<IMyInterface> mock = new Mock<IMyInterface>();
mock.Setup(m => m.ProcessMessage("hello")).Returns("Hi there");
mock.Setup(m => m.ProcessMessage("bye")).Returns("See you soon");
这里当我们的传入"hello"会返回"Hi there",传入"bye"会返回"See you soon",对于其他的参数会返回一个默认值,这里的默认值就是null,因为我们使用的是字符串类型的参数。很容易我们就会发现这种方式在遇到处理复杂的类型时就会很麻烦,因为我们会创建很多的对象来展示和做比较。幸运的是,Moq提供了It类,让我们能够呈现广泛的参数值的种类。比如这样:mock.Setup(m => m.ProcessMessage(It.IsAny<string>())).Returns("Message received");It类定义了许多使用泛型类型参数的 方法,这里的例子我们可以调用IsAny方法并使用string作为泛型参数。这就是让Moq知道当传入任何string值来调用ProcessMessage()方法时,它都会返回"Message received".下图展示了It类提供的方法列表:
下面是一个使用It参数过滤器的例子,如下所示:
mock.Setup(m => m.ProcessMessage(It.Is<string>(s => s == "hello" || s == "bye"))).Returns("Message received");
这条语句指示Moq返回"Message received"如果string参数是"hello"或"bye"
当我们设定一个行为(behavior)时,常常会定义一个方法被调用时的返回结果。在上面的代码里面我们是链接Returns在方法的最后.我们也可以使用传入Mocked方法里面的参数作为Returns()的参数,这样可以得到一个基于输入的输出。如下面的代码所示:
mock.Setup(m => m.ProcessMessage(It.IsAny<string>())).Returns<string>(s => string.Format("Message received: {0}", s));
下面我可以具体的在单元测试里面来使用Moq,如下代码所示:
[TestMethod]
public void Correct_Total_Reduction_Amount() {
// Arrange
Product[] products = new Product[]
{
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}
};
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.GetProducts()).Returns(products);
decimal reductionAmount = 10;
decimal initialTotal = products.Sum(p => p.Price);
MyPriceReducer target = new MyPriceReducer(mock.Object);
// Act
target.ReducePrices(reductionAmount);
// Assert
Assert.AreEqual(products.Sum(p => p.Price),(initialTotal - (products.Count() * reductionAmount)));
}
在上面的代码里面实现了GetProducts()来返回我们的测试数据,并且我们把所有的事情都放在了一个单元测试的方法里面。其实我们可以使用VS提供给我们的一些测试功能来让事情变得更加简单。比如这里,我们知道所有的测试方法都将使用测试对象Product,所有可以将它创建为测试类的一个初始化方法,如下所示:
[TestClass]
public class MyPriceReducerTest
{
private IEnumerable<Product> products;
[TestInitialize] //这个特性能够让VS的测试功能来初始化数据
public void PreTestInitialize()
{
products = new Product[] {
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}
};
}
......
VS的测试功能除了提供了TestInitialize(测试执行前调用)特性,还有ClassInitialize(单元测试执行之前调用,必须用于静态方法),ClassCleanup(所有的单元测试执行完后调用,必须用于静态方法),TestCleanup(在测试执行后调用)。
Verifying with Moq(Moq验证):在我们测试规则有一条是UpdateProduct()方法会被每一个Product对象调用。在FakeRepository类里面,我们是在其方法里面定义了一个
自增的属性实现的。代码是这样的:
public void UpdateProduct(Product product)
{
foreach (Product p in products.Where(e => e.Name == product.Name).Select(e => e))
{
p.Price = product.Price;
}
UpdateProductCallCount++;
}
public int UpdateProductCallCount { get; set; }
这里我们可以使用Moq通过一种更加优雅的方式来实现同样的效果,如下所示:
// Act
target.ReducePrices(reductionAmount);
// Assert
foreach (Product p in products) {
mock.Verify(m => m.UpdateProduct(p), Times.Once()); //验证每个Product对象调用了UpdateProduct()方法一次
}
}
到这里关于MVC的三个工具就介绍完了,后面的笔记进入一个项目实例。
好了,今天的笔记做到这里。
晚安!