使用 NUnit Mocking .NET 对象
原文名称:Mocking .NET Objects with NUnit
NUnit 是我进行 .NET 开发的单元测试工具,Microsoft 也提供了一个测试框架,但是只能与高版本的 Visual Studio 一起工作,这个框架与 NUnit 非常相像。在 Java 中,通过 Mocking 来帮助测试非常方便,我曾经写过一篇使用 Java 的文章 using JMock for Unit Tesing,在这里,我会讨论一下 NUnit 现在支持的开箱即用的 Mock 支持。
什么是 Mock 对象?
Mock 对象是用来隔离测试依赖目标的技术,通过隔离使得我们可以细粒度地测试单个类中的单个方法, 可能在依赖的类完全实现之前进行。隔离可以使你的测试更快进行,使得测试一小片功能更加容易。当你通过隔离完成分离的一部分测试之后,就可以有信心进行更大粒度的测试,当你在处理像数据访问层或者 Web 服务层的依赖的时候,隔离会更有意义,外部的调用会花费很多的时间,或者在远程系统当机的时候可能会失败。
对于使用 Mock 对象来说,重要的是逼迫你考虑你的类和方法依赖的东西,它强迫你考虑你的类之间的耦合性,如果耦合性很高的话,就很难测试,如果耦合性弱的话,就非常容易测试。有关这些早期概念设计的考虑可以帮助你更加轻松地管理随时间推移带来变化。
要点
- 好的设计强于坏的设计
- 低耦合的对象经常好于强耦合的对象
- 随着时间的推移,测试可以提高代码的质量和开发效率
- 低耦合的设计容易测试
示例项目
下面我们使用一个例子来演示一下,我们创建一个 Person 的领域对象,和一个名为 IPersonRepository 的数据访问接口,这时候非常简单。
public class Person { public string Id; public string FirstName; public string LastName; public Person(string newId, string fn, string ln) { Id = newId; FirstName = fn; LastName = ln; } }
接口的定义
public interface IPersonRepository { List<Person> GetPeople(); Person GetPersonById(string id); }
然后,我们创建一个 PersonService 类,在我们的程序中,用来实现业务逻辑,它将使用数据访问层,然后为 UI 层返回信息。
我们使用 DI 的构造函数注入来把他们连接在一起,所有依赖的对象都通过构造函数来传送。这样 PersonService 除了接口之外,就不需要知道具体的实现类,以实现低耦合。这样,在构造函数中我们不能传递一个无效的 PersonService 。
下面是一个简单的实现,但是我希望它能够表达它的使用。
public class PersonService { private IPersonRepository personRepos; public PersonService(IPersonRepository repos) { personRepos = repos; } public List<Person> GetAllPeople() { return personRepos.GetPeople(); } public List<Person> GetAllPeopleSorted() { List<Person> people = personRepos.GetPeople(); people.Sort(delegate(Person lhp, Person rhp) { return lhp.LastName.CompareTo(rhp.LastName); }); return people; } public Person GetPerson(string id) { try { return personRepos.GetPersonById(id); } catch (ArgumentException) { return null; // no person with that id was found } } }
使用 NUnit 进行 Mock
现在,我们可以开始测试我们的 PersonService。甚至我们还没有任何 IPersonRepository 的实现,使用 Mock 我们不需要考虑应用程序中其他层,就可以检查我们的 PersonService 是否如我们希望的完成任何任务。
using System; using System.Collections.Generic; using NUnit.Framework; using NUnit.Mocks; [TestFixture] public class PersonServiceTest { // 我们用来实现 IPersonRepository 的动态代理 private DynamicMock personRepositoryMock; // 一些测试数据 private Person onePerson = new Person("1", "Wendy", "Whiner"); private Person secondPerson = new Person("2", "Aaron", "Adams"); private List<Person> peopleList; [SetUp] public void TestInit() { peopleList = new List<Person>(); peopleList.Add(onePerson); peopleList.Add(secondPerson); // 使用接口 IPersonRepository 创建动态 Mock 对象 personRepositoryMock = new DynamicMock(typeof (IPersonRepository)); } [Test] public void TestGetAllPeople() { // 当调用 "GetPeople" 方法的时候,返回一个 // 预定义列表
personRepositoryMock.ExpectAndReturn("GetPeople", peopleList); // 使用 IPersonRepository 的 Mock 对象来创建 PersonService PersonService service = new PersonService( (IPersonRepository) personRepositoryMock.MockInstance); // 调用方法并进行断言 Assert.AreEqual(2, service.GetAllPeople().Count); } [Test] public void TestGetAllPeopleSorted() { personRepositoryMock.ExpectAndReturn("GetPeople", peopleList); PersonService service = new PersonService( (IPersonRepository) personRepositoryMock.MockInstance); // This method really has "business logic" in it - the sorting of people List<Person> people = service.GetAllPeopleSorted(); Assert.IsNotNull(people); Assert.AreEqual(2, people.Count); // Make sure the first person returned is the correct one Person p = people[0]; Assert.AreEqual("Adams", p.LastName); } [Test] public void TestGetSinglePersonWithValidId() { // 当调用 "GetPerson" 方法的时候返回一个预定义的 Person personRepositoryMock.ExpectAndReturn("GetPersonById", onePerson, "1"); PersonService service = new PersonService( (IPersonRepository) personRepositoryMock.MockInstance); Person p = service.GetPerson("1"); Assert.IsNotNull(p); Assert.AreEqual(p.Id, "1"); } [Test] public void TestGetSinglePersonWithInalidId() { // 当传递一个 null 调用 "GetPersonById" 方法的时候 // 抛出异常 ArgumentException personRepositoryMock.ExpectAndThrow("GetPersonById", new ArgumentException("Invalid person id."), null); PersonService service = new PersonService( (IPersonRepository) personRepositoryMock.MockInstance); // The only way to get null is if the underlying IPersonRepository // threw an ArgumentException Assert.IsNull(service.GetPerson(null)); } }
现在的 PersonService 没有什么真正的逻辑,我希望它能帮助你理解如何简单地通过 Mock 对象来进行各种条件的测试。
虽然 NUnit 内置的 Mock 库不是最强大的 Mock 库,还是应该充分使用它,我相信随着时间的推移,它将会变得更加强大。