在某些情况下(尤其是对void方法),检测替代实例是否能成功接收到一个特定的调用是非常有用的。可以通过使用 Received() 扩展方法,并紧跟着被检测的方法。
1 public interface ICommand 2 { 3 void Execute(); 4 event EventHandler Executed; 5 } 6 7 public class SomethingThatNeedsACommand 8 { 9 ICommand command; 10 public SomethingThatNeedsACommand(ICommand command) 11 { 12 this.command = command; 13 } 14 public void DoSomething() { command.Execute(); } 15 public void DontDoAnything() { } 16 } 17 18 [TestMethod] 19 public void Test_CheckReceivedCalls_CallReceived() 20 { 21 //Arrange 22 var command = Substitute.For<ICommand>(); 23 var something = new SomethingThatNeedsACommand(command); 24 25 //Act 26 something.DoSomething(); 27 28 //Assert 29 command.Received().Execute(); 30 }
在这个例子中,command 收到了一个对 Execute() 方法的调用,所以会顺利地完成。如果没有收到对 Execute() 的调用,则 NSubstitute 会抛出一个 ReceivedCallsException 异常,并且会显示具体是在期待什么方法被调用,参数是什么,以及列出实际的方法调用和参数。
检查一个调用没有被收到
通过使用 DidNotReceive() 扩展方法,NSubstitute 可以确定一个调用未被接收到。
1 [TestMethod] 2 public void Test_CheckReceivedCalls_CallDidNotReceived() 3 { 4 //Arrange 5 var command = Substitute.For<ICommand>(); 6 var something = new SomethingThatNeedsACommand(command); 7 8 //Act 9 something.DontDoAnything(); 10 11 //Assert 12 command.DidNotReceive().Execute(); 13 }
检查接收到指定次数的调用
Received() 扩展方法会对某成员的至少一次的调用进行断言,DidNotReceive() 则会断言未收到调用。NSubstitute 也提供允许断言某调用是否接收到了指定的次数的选择,通过传递一个整型值给 Received() 方法。如果替代实例没有接收到给定的次数,则将会抛出异常。接收到的次数少于或多于给定次数,断言会失败。
1 public class CommandRepeater 2 { 3 ICommand command; 4 int numberOfTimesToCall; 5 public CommandRepeater(ICommand command, int numberOfTimesToCall) 6 { 7 this.command = command; 8 this.numberOfTimesToCall = numberOfTimesToCall; 9 } 10 11 public void Execute() 12 { 13 for (var i = 0; i < numberOfTimesToCall; i++) command.Execute(); 14 } 15 } 16 17 [TestMethod] 18 public void Test_CheckReceivedCalls_CallReceivedNumberOfSpecifiedTimes() 19 { 20 // Arrange 21 var command = Substitute.For<ICommand>(); 22 var repeater = new CommandRepeater(command, 3); 23 24 // Act 25 repeater.Execute(); 26 27 // Assert 28 // 如果仅接收到2次或者4次,这里会失败。 29 command.Received(3).Execute(); 30 }
Received(1) 会检查该调用收到并且仅收到一次。这与默认的 Received() 不同,其检查该调用至少接收到了一次。Received(0) 的行为与 DidNotReceive() 相同。
接收到或者未接收到指定的参数
我们也可以使用参数匹配器来检查是否收到了或者未收到包含特定参数的调用。在参数匹配器一节会介绍更多细节,下面的例子演示了一般用法:
1 public interface ICalculator 2 { 3 int Add(int a, int b); 4 string Mode { get; set; } 5 } 6 7 [TestMethod] 8 public void Test_CheckReceivedCalls_CallReceivedWithSpecificArguments() 9 { 10 var calculator = Substitute.For<ICalculator>(); 11 12 calculator.Add(1, 2); 13 calculator.Add(-100, 100); 14 15 // 检查接收到了第一个参数为任意值,第二个参数为2的调用 16 calculator.Received().Add(Arg.Any<int>(), 2); 17 // 检查接收到了第一个参数小于0,第二个参数为100的调用 18 calculator.Received().Add(Arg.Is<int>(x => x < 0), 100); 19 // 检查未接收到第一个参数为任意值,第二个参数大于等于500的调用 20 calculator 21 .DidNotReceive() 22 .Add(Arg.Any<int>(), Arg.Is<int>(x => x >= 500)); 23 }
忽略参数
就像我们可以为任意参数设置返回值一样,NSubstitute 可以检查收到或者未收到调用,同时忽略其中包含的参数。此时我们需要使用 ReceivedWithAnyArgs() 和 DidNotReceiveWithAnyArgs()。
1 [TestMethod] 2 public void Test_CheckReceivedCalls_IgnoringArguments() 3 { 4 var calculator = Substitute.For<ICalculator>(); 5 6 calculator.Add(1, 3); 7 8 calculator.ReceivedWithAnyArgs().Add(1, 1); 9 calculator.DidNotReceiveWithAnyArgs().Subtract(0, 0); 10 }
检查对属性的调用
同样的语法可以用于检查属性的调用。通常情况下,或许我们会避免这种检测,可能我们对所需的行为进行测试更感兴趣,而不是实现细节的精确性(例如,我们可以设置一个属性返回一个值,然后检测该值是否被合理的使用,而不是断言该属性的 getter 被调用了)。当然,有些时候检查 getter 和 setter 是否被调用仍然会派的上用场,所以,这里会介绍如何使用该功能:
1 [TestMethod] 2 public void Test_CheckReceivedCalls_CheckingCallsToPropeties() 3 { 4 var calculator = Substitute.For<ICalculator>(); 5 6 var mode = calculator.Mode; 7 calculator.Mode = "TEST"; 8 9 // 检查接收到了对属性 getter 的调用 10 // 这里需要使用临时变量以通过编译 11 var temp = calculator.Received().Mode; 12 13 // 检查接收到了对属性 setter 的调用,参数为"TEST" 14 calculator.Received().Mode = "TEST"; 15 }
检查调用索引器
其实索引器只是另外一个属性,所以我们可以使用相同的语法来检查索引器调用。
1 [TestMethod] 2 public void Test_CheckReceivedCalls_CheckingCallsToIndexers() 3 { 4 var dictionary = Substitute.For<IDictionary<string, int>>(); 5 dictionary["test"] = 1; 6 7 dictionary.Received()["test"] = 1; 8 dictionary.Received()["test"] = Arg.Is<int>(x => x < 5); 9 }
检查事件订阅
与属性一样,我们通常更赞成测试所需的行为,而非检查对特定事件的订阅。可以通过使用在替代实例上引发一个事件的方式,并且断言我们的类在响应中执行的正确的行为:
1 public class CommandWatcher 2 { 3 ICommand command; 4 public CommandWatcher(ICommand command) 5 { 6 this.command = command; 7 this.command.Executed += OnExecuted; 8 } 9 public bool DidStuff { get; private set; } 10 public void OnExecuted(object o, EventArgs e) 11 { 12 DidStuff = true; 13 } 14 } 15 16 [TestMethod] 17 public void Test_CheckReceivedCalls_CheckingEventSubscriptions() 18 { 19 var command = Substitute.For<ICommand>(); 20 var watcher = new CommandWatcher(command); 21 22 command.Executed += Raise.Event(); 23 24 Assert.IsTrue(watcher.DidStuff); 25 }
当然,如果需要的话,Received() 会帮助我们断言订阅是否被收到:
1 [TestMethod] 2 public void Test_CheckReceivedCalls_MakeSureWatcherSubscribesToCommandExecuted() 3 { 4 var command = Substitute.For<ICommand>(); 5 var watcher = new CommandWatcher(command); 6 7 // 不推荐这种方法。 8 // 更好的办法是测试行为而不是具体实现。 9 command.Received().Executed += watcher.OnExecuted; 10 // 或者有可能事件处理器是不可访问的。 11 command.Received().Executed += Arg.Any<EventHandler>(); 12 }