前面说了手工创建模拟对象的示例,这一章主要说一下使用模拟框架创建示例。我使用的模拟框架是NSubstitute
1:如何创建伪对象
我们知道手动编写伪对象显得很笨拙(当然按照实际情况),那么用隔离框架创建伪对象就会很简单快速,那么我们先看一幅图
那么首先我们创建一个个接口
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public interface IUser 2 { 3 bool Add(string userName, string realName); 4 }
实现接口
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class User:IUser { 2 public bool Add(string userName, string realName) 3 { 4 return true; 5 } 6 }
业务中调用
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class UserManager 2 { 3 public bool Add(string userName, string realName) 4 { 5 if (string.IsNullOrWhiteSpace(userName))throw new ArgumentNullException(userName); 6 if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentNullException(realName); 7 8 if (userName.Length>=0) 9 { 10 return false; 11 } 12 return GetUser().Add(userName, realName); 13 } 14 }
单元测试
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 [Test] 2 public void Add_Default_CallAdd() 3 { 4 var fakeUser = Substitute.For<IUser>(); 5 fakeUser.Add("1", "1").Returns(true);//按照我们的意愿进行返回值 6 Assert.IsTrue(fakeUser.Add("1", "1")); 7 }
以上是我们输入2个参数然后按我们意愿返回true;
如果我们不考虑参数呢
那么只需要把参数置为Arg.Any<T>()//T表示参数的类型可以string,int等看下测试结果
当然返回值一定是按照你预定的类型。比喻bool Add() 返回值必须是一个bool类型的或者抛出异常。
2:When...Do的用法
when do在NSub的用处还是比较广的。比喻当什么的时候做什么。下面一个例子来看一下
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 [Test] 2 public void Add_Default_CallAdd() 3 { 4 var fakeUser = Substitute.For<IUser>(); 5 fakeUser.When(x => x.Add("1", "2")). 6 Do(content => { throw new Exception("fack exception"); });//用when,do表示式,表示当操作这个函数的时候会出现什么后果 7 Assert.Throws<Exception>(() => fakeUser.Add("1","2")); 8 }
上面的例子就是当参数是1和2的时候会抛出一个异常。下面看现实的结果
刚刚参数是1和2会抛出一个异常如果是1和3会不会抛出异常呢,我们手动把参数进行修改在看下面效果
我们一看测试通不过,此时说明只有当参数1和2的时候才会抛出异常这样一来模拟框架也会给我们预定的参数进行一个验证。
3:很强大的Received()
Received()方法工作很神奇,这个方法在什么对象被调用就会返回和这个对象同样类型的对象,但实际上就是申明断言的对象。什么意思呢我们看下面的代码
测试中的接口和实现方法参考上一篇
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 [Test] 2 public void RecordLog_EmailServiceThrows_CallEmail() 3 { 4 var stub = Substitute.For<ILogService>(); 5 var mock = Substitute.For<IEmailService>(); 6 stub.When(p => p.ErrorLog(Arg.Any<String>())).Do(a => { throw new ArgumentNullException(); }); 7 var test = new TestUserManager(stub, mock); 8 test.RecordLog("");//触发 9 mock.Received().SendEmail(Arg.Is("lp"), Arg.Is("subject"), Arg.Is("值不能为 null。"));//保证接受了方法并调用(不可以少一个句号) 10 }
在以上的例子我们可以看出Received()就表示SendEmail被调用并且收获到SendEmail发送的信息然后进行断言是否和预定发送的信息保持一致。这个和上一篇说的模拟对象断言是一样的。(如果使用模拟对象不要忘记Received)
4:事件应该怎么测试
- 测试监听事件的一方
- 测试触发事件的一方
4.1:测试监听事件
定义一个事件的接口
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public interface IView 2 { 3 event Action Loaded; 4 void Render(string content); 5 }
事件相关的代码并如何触发事件的
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class Presenter 2 { 3 4 private readonly IView _view; 5 6 public Presenter(IView view) 7 { 8 this._view = view; 9 this._view.Loaded += Onload;//注册事件 10 } 11 12 public void Onload() 13 { 14 _view.Render("Hello World"); 15 } 16 }
测试代码
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 [Test] 2 public void Ctor_WhenViewIsLoad_CallsViewRender() 3 { 4 var mockView = Substitute.For<IView>(); 5 var p =new Presenter(mockView); 6 mockView.Loaded += Raise.Event<Action>(); 7 8 mockView.Received().Render(Arg.Is<string>(s=>s.Contains("Hello World"))); 9 10 }
Raise.Event表示触发事件。
最后使用received来测试是否view中的Render是否触发,如果真的触发那么接受的参数是否是Hello World。
我们可以模拟一个存根一个一个模拟对象
修改与事件相关的代码
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class Presenter 2 { 3 4 private readonly IView _view; 5 private readonly ILogService _logService; 6 7 public Presenter(IView view,ILogService logService) 8 { 9 this._view = view; 10 this._logService = logService; 11 this._view.Loaded += Onload;//注册事件 12 this._view.ErrorOccured += WriteLog; 13 } 14 15 public void Onload() 16 { 17 _view.Render("Hello World"); 18 } 19 20 public void WriteLog(string errorContent) { 21 _logService.ErrorLog(errorContent); 22 } 23 }
测试代码
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 [Test] 2 public void Ctor_WhenViewIsError_CallsLogs() { 3 4 var stubView = Substitute.For<IView>(); 5 var mockLog = Substitute.For<ILogService>(); 6 var p = new Presenter(stubView,mockLog); 7 stubView.ErrorOccured += Raise.Event<Action<string>>("fack error"); 8 mockLog.Received().ErrorLog(Arg.Is<string>(s => s.Contains("fack error"))); 9 10 }
注意:使用存根stubView.ErrorOccured += Raise.Event<Action<string>>("fack error");来触发事件,然后在用模拟对象查看日志服务是否被正确的调用
4.2:测试事件是否触发
这个比较简单,在测试方法内部使用一个匿名委托,手动注册次方法即可
定义一个变量,当事件触发改变变量的值即可。
如:bool isTrigger=false;
SomeView=new SomeView();
SomeView.Load+=delegate{isTrigger=true;}
SomeView.触发事件 然后比对isTrigger的值是否改变。
5:模拟框架的优缺点
- 优点
- 容易验证参数
- 容易创建伪对象
- 使用模拟对象的时候比较简单不用手写大量代码
- 缺点
测试代码不可读
验证错误的事情
一个测试可能会有多个模拟对象
过度指定
NSubstitute的官方文档很详细有兴趣的可以自己查看动手写一写会有更好的效果http://nsubstitute.github.io/help/raising-events/