Mock Framework 实战

今晚的世博开幕式很棒,欢迎大家到上海来看Expo!

上一篇中和大家分享了单元测试的理论、入门和一些实践(更多的实践会逐步更新进来),这篇中来介绍下更多的应用场景和使用Mock对象来进行快速的生成模拟对象来简化测试并解决一些问题。

场景分析:
   

我们假设一个应用场景,也是发生在项目中真实的案例。
  
  • 真实对象不存在或不完备:新产品开发时,Platform Team只定义了接口,并未完全实现该接口中的方法。但Feature Team仍要进行自己的开发,并需要底层数据支持,等Platform开发完成再去编写自己的代码显然是不可能的,如何去处理这种依赖?
  • 真实对象本身不确定:底层接品在设计之初也是不稳定的,通常在产品一个方法的返回值会跟据需求经常变动(这种情况在好的设计中出现的概率较小),如何在这种情况下保证上层代码的安全。
  • 数据源不稳定:有些依赖数据会经常变化,或者根本拿不到,但方法本身仅是对数据的加工,不涉及取数据。
  • 代码依赖:当要去初始化一个对象时,要初始化另外的依赖对象,还要解决依赖对象的依赖对象…,还不算完,一旦这个测试挂掉,恭喜,你又一次陷入穷尽的 Debug中,因为无从只到是哪一步出错。

解决方案

    

上一篇示例中已经提到这个问题,并通过一个简单的类来代替当前这个真实而复杂的类,返回我们期望的数据。这样做的好处,一个测试出错,总能立即找到它对应的方法,不用对外部类的行为负责任,因为单元测试是以方法为单位的,每一个类的方法都应被测试并保证返回结果的正确性,所以不必担心这样会产生测试不完整的问题。

  • 为该对象创建一个接口(如果已有,跳过前两步)
  • 让真实对象实现该接口
  • 在测试中,创建一个Mock的对象实现这个接口
  • 对要测试的方 法进行指定返回值
  • 用Mock对象代替真实对象完成方法执行

1 public interface IUserService
2 {
3     User GetUser(string id);
4 
5     IList<User> GetUsers();
6 }


实现类UserService 调用了IUserRepository接口中的GetUser方法,但不对任何具体的实现类产生依赖。这就是一直鼓励的面向接口编程

 1 public class UserService : IUserService
 2 {
 3     private IUserRepository userRepository { getset; }
 4 
 5     public UserService(IUserRepository userRepository)
 6     {
 7         this.userRepository = userRepository;
 8     }
 9 
10     #region IUserService Members
11 
12     public User GetUser(string id)
13     {
14         return this.userRepository.GetUser(id);
15     }
16 
17     public IList<User> GetUsers()
18     {
19         return this.userRepository.GetUsers();
20     }
21 
22     #endregion
23 }


下面是IUserRepository接口的代码

1 public interface IUserRepository
2 {
3     List<User> GetUsers();
4     User GetUser(string Id);
5 
6     void Save(User user);
7 }

 

紧接着的是我们对IUserRepository接口所创建的Mock类,可以看一下我们很简单的处理了下GetUser方法,让TA返回一个指定值User对象。

 

 1 public class MockUserRepository : IUserRepository
 2 {
 3     public List<Core.Models.User> GetUsers()
 4     {
 5         throw new NotImplementedException();
 6     }
 7 
 8     public Core.Models.User GetUser(string Id)
 9     {
10         return new User() { Id = "3277", Name = "Lanvige" };
11     }
12 
13     public void Save(Core.Models.User user)
14     {
15         throw new NotImplementedException();
16     }
17 }


下面的代码是真正的测试用例代码

 1 [TestClass]
 2 public class UserServiceTest
 3 {
 4     private IUserRepository userRepository;
 5     private UserService userService;
 6 
 7     public UserServiceTest()
 8     {
 9         userRepository = new MockUserRepository();
10         userService = new UserService(userRepository);
11     }
12 
13     [TestMethod]
14     public void Get_User_By_Id_Test()
15     {
16         User user = this.userService.GetUser("3277");
17 
18         Assert.AreEqual("3277", user.Id);
19         Assert.AreEqual("Lanvige", user.Name);
20     }
21 }


这样,我们就解决了在单元测试中的一些难题,而Mock的理念也得到充分展示,模拟真实对象的方法,返回所需的值,让测试变得更简单。

但上面的解决方案的实现还是很复杂的,接口我们需要自已动手写另一个实现类,实现接口中所有的方法,虽然有些我们并不需要。如果我们需要更多的验证时,就要去写更多的代码来实现,如判断代码是否被调用和调用次数,传入参数的正确性和忽略实参……

有没有更简单的方法
    

有没有一种通用的方法对生成这些类,减少单元测试中的代码量和复杂度,有,那就是Mock Framework。

Mock Framework主要是用来模拟那些在应用中不容易构造或者比较复杂的对象从而使测试顺利进行的工具。够根据现有的接口或类动态生成Mock对象,不仅能避免额外的编码工作,同时也降低了引入错误的可能。

  • 可以用简单易行的方法定义模拟对象,无需破坏本来的代码结构表;
  • 可以定义对象之间的交互,从而增强测试的稳定性;
  • 可以集成到测试框架;
  • 易扩充; 

比较著名的Mock框架有Rhino Mocks, Moq等,其中Rhino的功能较为强大,而Moq新秀则使用Lambda表达式使得语义更加清晰而日渐受到欢迎。我们今天使用Moq来做一下简单的Mock的使用,当然,这里介绍的还是思想,这里的方法你用其它的Mock框架也是能使用的,只是方法名,变量可能会因不同框架而不同。

使用Moq
    

对上面代面的另一种写法。

 1 [TestClass]
 2 public class UserServiceTestWithMoq
 3 {
 4     private IUserRepository userRepository;
 5     private UserService userService;
 6 
 7     [TestMethod]
 8     public void Get_User_By_Id_Test()
 9     {
10         var mockUserRepository = new Mock<IUserRepository>();
11         mockUserRepository.Setup(x => x.GetUser(It.IsAny<string>()))
12             .Returns(new User() { Id = "3277", Name = "Lanvige" });
13         mockUserRepository.Verify(x => x.GetUser(It.IsAny<string>()), Times.AtLeastOnce());
14 
15         userRepository = mockUserRepository.Object;
16         userService = new UserService(userRepository);
17 
18         User user = this.userService.GetUser("3");
19 
20         Assert.AreEqual("3277", user.Id);
21         Assert.AreEqual("Lanvige", user.Name);
22     }
23 }


通过代码我们可以看到,在之前手工去Mock时的一个对象,在这里只用两句话就建立起相同的内容,Mock一个对象,并对其中的GetUser方法进行指定返回值,大大的提高了我们的工作效率。

并且我们使用了mockUserRepository.Verify来验证该代码是否被执行,比手工Mock对象代码量少并进行了更多的验证。

代码中哪些需要被Mock,以及Mock的更多作用
  

  • 方法:在上面的代码中我们已经Mock了一个对象并反回指定的对象。值得注意的是It.IsAny<string>()这句话,它的意思是不去匹配所传参数,当然也有具体匹配,范围匹配。
  • 属性:方法中的属性仅用于约束属性的作用范围,不具有真实的存值空间,所以Mock对象无法直接为属性赋值,要通过Mock来实现。
  • 事件:为对象中的事件进行绑定新的行为或删除已有行为。
  • 回调:在方法结束后去调用一个指定方法。
  • 验证:这里的验证和我们在Test Framework中的Assert不大一样,它验证的是Mock对象在执行过程中的一些行为和属性,例如方法是否被调用,调用次数、属性取值是否为期望值,Set方法是否将值赋给对象的属性。

不要过多依赖
   

体验完Mock之后,是不是觉得很Cool,有一种想把一切都Mock起来的冲动。很诱人,但也并非完全适用,

开发者对API的了解不够、被模拟对象的行为发生变化(重构、添加新功能等修改等都可能引起被被模拟对象的行为变化)都可能导致错误假设(与真实对象行为 不一致),错误假设会悄无声息的引入缺陷并留下非法测试。

 

下一篇中,我们会对Moq进行更多的深入,你会学到如何使用Moq进行Mock属性,方法,事件,回调及验证。

 

Zhiming
MSN GMD Shanghai

posted @ 2010-05-01 00:58  Zhiming Jiang  阅读(3363)  评论(16编辑  收藏  举报