.NET测试--模拟框架NSubstitute
.NET测试--模拟框架NSubstitute
NSubstitute在GitHub的开源地址:https://github.com/nsubstitute/nsubstitute/downloads
入门
现在假设我们有一个基本的计算器界接口:
- public interface ICalculator
- {
- int Add(int a, int b);
- string Mode { get; set; }
- event EventHandler PoweringUp;
- }
使用NSubstitute创建一个替代实例。
我们可以使用stub(存根)、mock(模拟)、fake、spy、test double等。
- calculator = Substitute.For<ICalculator>();
现在我们可以使用calculator对象返回一个计算值:
- calculator.Add(1, 2).Returns(3);
- Assert.That(calculator.Add(1, 2), Is.EqualTo(3));
检测calculator对象是否执行了一个方法调用,没有执行其他的调用:
- calculator.Add(1, 2);
- calculator.Received().Add(1, 2);//是否执行了调用
- calculator.DidNotReceive().Add(5, 7);//是否没有执行调用
使用Returns方法,返回执行调用的返回值。
- calculator.Mode.Returns("DEC");
- Assert.That(calculator.Mode, Is.EqualTo("DEC"));
-
- calculator.Mode = "HEX";
- Assert.That(calculator.Mode, Is.EqualTo("HEX"));
NSubstitute 支持设置输入参数匹配:
- calculator.Add(10, -5);
- calculator.Received().Add(10, Arg.Any<int>());//任何int参数
- calculator.Received().Add(10, Arg.Is<int>(x => x < 0));//第二个参数小于零
使用参数匹配以及讲函数传递给Returns方法:
- calculator
- .Add(Arg.Any<int>(), Arg.Any<int>())
- .Returns(x => (int)x[0] + (int)x[1]);
- Assert.That(calculator.Add(5, 10), Is.EqualTo(15));
Returns方法返回多参数序列:
- calculator.Mode.Returns("HEX", "DEC", "BIN");
- Assert.That(calculator.Mode, Is.EqualTo("HEX"));
- Assert.That(calculator.Mode, Is.EqualTo("DEC"));
- Assert.That(calculator.Mode, Is.EqualTo("BIN"));
引发事件:
- bool eventWasRaised = false;
- calculator.PoweringUp += (sender, args) => eventWasRaised = true;
- calculator.PoweringUp += Raise.Event();
- Assert.That(eventWasRaised);
创建substitute
创建一个具有构造函数参数的类的替代:
- var someClass = Substitute.For<SomeClassWithCtorArgs>(5, "hello world");
多接口
- var command = Substitute.For<ICommand, IDisposable>();
- var runner = new CommandRunner(command);
-
- runner.RunCommand();
-
- command.Received().Execute();
- ((IDisposable)command).Received().Dispose();
可以设置多接口,但是类只能有一个:
- var substitute = Substitute.For(
- new[] { typeof(ICommand), typeof(ISomeInterface), typeof(SomeClassWithCtorArgs) },
- new object[] { 5, "hello world" }
- );
- Assert.IsInstanceOf<ICommand>(substitute);
- Assert.IsInstanceOf<ISomeInterface>(substitute);
- Assert.IsInstanceOf<SomeClassWithCtorArgs>(substitute);
代理
- var func = Substitute.For<Func<string>>();
- func().Returns("hello");
- Assert.AreEqual("hello", func());
设置返回值
- public interface ICalculator {
- int Add(int a, int b);
- string Mode { get; set; }
- }
方法
- var calculator = Substitute.For<ICalculator>();
- calculator.Add(1, 2).Returns(3);
调用的参数必须一样,返回值才会固定
- //Make a call return 3:
- calculator.Add(1, 2).Returns(3);
- Assert.AreEqual(calculator.Add(1, 2), 3);
- Assert.AreEqual(calculator.Add(1, 2), 3);
-
- //Call with different arguments does not return 3
- Assert.AreNotEqual(calculator.Add(3, 6), 3);
属性
- calculator.Mode.Returns("DEC");
- Assert.AreEqual(calculator.Mode, "DEC");
- calculator.Mode = "HEX";
- Assert.AreEqual(calculator.Mode, "HEX");
返回值指定输入具体参数
- //Return when first arg is anything and second arg is 5:
- //返回当第一个参数是任意int,第二个参数是5
- calculator.Add(Arg.Any<int>(), 5).Returns(10);
- Assert.AreEqual(10, calculator.Add(123, 5));
- Assert.AreEqual(10, calculator.Add(-9, 5));
- Assert.AreNotEqual(10, calculator.Add(-9, -9));
-
- //Return when first arg is 1 and second arg less than 0:
- //返回当第一个参数是1,第二个参数小于零
- calculator.Add(1, Arg.Is<int>(x => x < 0)).Returns(345);
- Assert.AreEqual(345, calculator.Add(1, -2));
- Assert.AreNotEqual(345, calculator.Add(1, 2));
-
- //Return when both args equal to 0:
- //返回当两个参数都等于0
- calculator.Add(Arg.Is(0), Arg.Is(0)).Returns(99);
- Assert.AreEqual(99, calculator.Add(0, 0));
返回值指定输入任意参数
- calculator.Add(1, 2).ReturnsForAnyArgs(100);
- Assert.AreEqual(calculator.Add(1, 2), 100);
- Assert.AreEqual(calculator.Add(-7, 15), 100);
从一个方法获取返回值
- calculator
- .Add(Arg.Any<int>(), Arg.Any<int>())
- .Returns(x => (int)x[0] + (int)x[1]);//从一个方法获取返回值
-
- Assert.That(calculator.Add(1, 1), Is.EqualTo(2));
- Assert.That(calculator.Add(20, 30), Is.EqualTo(50));
- Assert.That(calculator.Add(-73, 9348), Is.EqualTo(9275));
调用信息
Arg<T>()
ArgAt<T>(int position)
- public interface IFoo {
- string Bar(int a, string b);
- }
-
- var foo = Substitute.For<IFoo>();
- foo.Bar(0, "").ReturnsForAnyArgs(x => "Hello " + x.Arg<string>());
- Assert.That(foo.Bar(1, "World"), Is.EqualTo("Hello World"));
回调
- var counter = 0;
- calculator
- .Add(0, 0)
- .ReturnsForAnyArgs(x => {
- counter++;
- return 0;
- });
-
- calculator.Add(7,3);
- calculator.Add(2,2);
- calculator.Add(11,-3);
- Assert.AreEqual(counter, 3);
- var counter = 0;
- calculator
- .Add(0, 0)
- .ReturnsForAnyArgs(x => 0)
- .AndDoes(x => counter++);//?
-
- calculator.Add(7,3);
- calculator.Add(2,2);
- Assert.AreEqual(counter, 2);
多返回值
- calculator.Mode.Returns("DEC", "HEX", "BIN");
- Assert.AreEqual("DEC", calculator.Mode);
- Assert.AreEqual("HEX", calculator.Mode);
- Assert.AreEqual("BIN", calculator.Mode);
多返回值回调
- calculator.Mode.Returns(x => "DEC", x => "HEX", x => { throw new Exception(); });
- Assert.AreEqual("DEC", calculator.Mode);
- Assert.AreEqual("HEX", calculator.Mode);
- Assert.Throws<Exception>(() => { var result = calculator.Mode; });
替换返回值
- calculator.Mode.Returns("DEC,HEX,OCT");
- calculator.Mode.Returns(x => "???");
- calculator.Mode.Returns("HEX");
- calculator.Mode.Returns("BIN");
- Assert.AreEqual(calculator.Mode, "BIN");
检查调用,是否执行,返回
- public interface ICommand {
- void Execute();
- event EventHandler Executed;
- }
-
- public class SomethingThatNeedsACommand {
- ICommand command;
- public SomethingThatNeedsACommand(ICommand command) {
- this.command = command;
- }
- public void DoSomething() { command.Execute(); }
- public void DontDoAnything() { }
- }
-
- [Test]
- public void Should_execute_command() {
- //Arrange
- var command = Substitute.For<ICommand>();
- var something = new SomethingThatNeedsACommand(command);
- //Act
- something.DoSomething();
- //Assert 检查是否执行了这个方法
- command.Received().Execute();
- }
检查一个调用是否未执行(未返回)
- var command = Substitute.For<ICommand>();
- var something = new SomethingThatNeedsACommand(command);
- //Act
- something.DontDoAnything();
- //Assert
- command.DidNotReceive().Execute();
检查一个调用,返回执行次数
- public class CommandRepeater {
- ICommand command;
- int numberOfTimesToCall;
- public CommandRepeater(ICommand command, int numberOfTimesToCall) {
- this.command = command;
- this.numberOfTimesToCall = numberOfTimesToCall;
- }
-
- public void Execute() {
- for (var i=0; i<numberOfTimesToCall; i++) command.Execute();
- }
- }
-
- [Test]
- public void Should_execute_command_the_number_of_times_specified() {
- var command = Substitute.For<ICommand>();
- var repeater = new CommandRepeater(command, 3);
- //Act
- repeater.Execute();
- //Assert
- command.Received(3).Execute(); // << This will fail if 2 or 4 calls were received
- }
带特殊参数的返回
- calculator.Add(1, 2);
- calculator.Add(-100, 100);
-
- //Check received with second arg of 2 and any first arg:
- calculator.Received().Add(Arg.Any<int>(), 2);
- //Check received with first arg less than 0, and second arg of 100:
- calculator.Received().Add(Arg.Is<int>(x => x < 0), 100);
- //Check did not receive a call where second arg is >= 500 and any first arg:
- calculator
- .DidNotReceive()
- .Add(Arg.Any<int>(), Arg.Is<int>(x => x >= 500));
忽略参数
- calculator.Add(1, 3);
-
- calculator.ReceivedWithAnyArgs().Add(1,1);
- calculator.DidNotReceiveWithAnyArgs().Subtract(0,0);
检查属性
- var mode = calculator.Mode;
- calculator.Mode = "TEST";
-
- //Check received call to property getter
- //We need to assign the result to a variable to keep
- //the compiler happy.
- var temp = calculator.Received().Mode;
-
- //Check received call to property setter with arg of "TEST"
- calculator.Received().Mode = "TEST";
检查索引
- var dictionary = Substitute.For<IDictionary<string, int>>();
- dictionary["test"] = 1;
-
- dictionary.Received()["test"] = 1;
- dictionary.Received()["test"] = Arg.Is<int>(x => x < 5);
检查订阅事件
- public class CommandWatcher {
- ICommand command;
- public CommandWatcher(ICommand command) {
- command.Executed += OnExecuted;
- }
- public bool DidStuff { get; private set; }
- public void OnExecuted(object o, EventArgs e) { DidStuff = true; }
- }
-
- [Test]
- public void ShouldDoStuffWhenCommandExecutes() {
- var command = Substitute.For<ICommand>();
- var watcher = new CommandWatcher(command);
-
- command.Executed += Raise.Event();
-
- Assert.That(watcher.DidStuff);
- }
- [Test]
- public void MakeSureWatcherSubscribesToCommandExecuted() {
- var command = Substitute.For<ICommand>();
- var watcher = new CommandWatcher(command);
-
- // Not recommended. Favour testing behaviour over implementation specifics.
- // Can check subscription:
- command.Received().Executed += watcher.OnExecuted;
- // Or, if the handler is not accessible:
- command.Received().Executed += Arg.Any<EventHandler>();
- }
清除调用
- public interface ICommand {
- void Execute();
- }
-
- public class OnceOffCommandRunner {
- ICommand command;
- public OnceOffCommandRunner(ICommand command) {
- this.command = command;
- }
- public void Run() {
- if (command == null) return;
- command.Execute();
- command = null;
- }
- }
- var command = Substitute.For<ICommand>();
- var runner = new OnceOffCommandRunner(command);
-
- //First run
- runner.Run();
- command.Received().Execute();
-
- //Forget previous calls to command
- //清楚前面的调用
- command.ClearReceivedCalls();
-
- //Second run
- runner.Run();
- command.DidNotReceive().Execute();
参数匹配
忽略参数
- calculator.Add(Arg.Any<int>(), 5).Returns(7);
-
- Assert.AreEqual(7, calculator.Add(42, 5));
- Assert.AreEqual(7, calculator.Add(123, 5));
- Assert.AreNotEqual(7, calculator.Add(1, 7));
-
-
- formatter.Format(new object());
- formatter.Format("some string");
-
- formatter.Received().Format(Arg.Any<object>());
- formatter.Received().Format(Arg.Any<string>());
- formatter.DidNotReceive().Format(Arg.Any<int>());
匹配一个参数
- calculator.Add(1, -10);
-
- //Received call with first arg 1 and second arg less than 0:
- calculator.Received().Add(1, Arg.Is<int>(x => x < 0));
- //Received call with first arg 1 and second arg of -2, -5, or -10:
- calculator
- .Received()
- .Add(1, Arg.Is<int>(x => new[] {-2,-5,-10}.Contains(x)));
- //Did not receive call with first arg greater than 10:
- calculator.DidNotReceive().Add(Arg.Is<int>(x => x > 10), -10);
-
- formatter.Format(Arg.Is<string>(x => x.Length <= 10)).Returns("matched");
-
- Assert.AreEqual("matched", formatter.Format("short"));
- Assert.AreNotEqual("matched", formatter.Format("not matched, too long"));
- // Will not match because trying to access .Length on null will throw an exception when testing
- // our condition. NSubstitute will assume it does not match and swallow the exception.
- Assert.AreNotEqual("matched", formatter.Format(null));
匹配特殊参数
- calculator.Add(0, 42);
-
- //This won't work; NSubstitute isn't sure which arg the matcher applies to:
- //calculator.Received().Add(0, Arg.Any<int>());
-
- calculator.Received().Add(Arg.Is(0), Arg.Any<int>());
-
使用参数匹配
- /* ARRANGE */
-
- var widgetFactory = Substitute.For<IWidgetFactory>();
- var subject = new Sprocket(widgetFactory);
-
- // OK: Use arg matcher for a return value:
- widgetFactory.Make(Arg.Is<int>(x => x > 10)).Returns(TestWidget);
-
- /* ACT */
-
- // NOT OK: arg matcher used with a real call:
- // subject.StartWithWidget(Arg.Any<int>());
-
- // Use a real argument instead:
- subject.StartWithWidget(4);
-
- /* ASSERT */
-
- // OK: Use arg matcher to check a call was received:
- widgetFactory.Received().Make(Arg.Is<int>(x => x > 0));
回调,空调用
- var counter = 0;
- calculator
- .Add(0,0)
- .ReturnsForAnyArgs(x => 0)
- .AndDoes(x => counter++);
-
- calculator.Add(7,3);
- calculator.Add(2,2);
- calculator.Add(11,-3);
- Assert.AreEqual(counter, 3);
when...do...
- public interface IFoo {
- void SayHello(string to);
- }
- [Test]
- public void SayHello() {
- var counter = 0;
- var foo = Substitute.For<IFoo>();
- foo.When(x => x.SayHello("World"))
- .Do(x => counter++);
-
- foo.SayHello("World");
- foo.SayHello("World");
- Assert.AreEqual(2, counter);
- }
复杂调用
- var sub = Substitute.For<ISomething>();
-
- var calls = new List<string>();
- var counter = 0;
-
- sub
- .When(x => x.Something())
- .Do(
- Callback.First(x => calls.Add("1"))
- .Then(x => calls.Add("2"))
- .Then(x => calls.Add("3"))
- .ThenKeepDoing(x => calls.Add("+"))
- .AndAlways(x => counter++)
- );
-
- for (int i = 0; i < 5; i++)
- {
- sub.Something();
- }
- Assert.That(String.Concat(calls), Is.EqualTo("123++"));
- Assert.That(counter, Is.EqualTo(5));
抛出异常
- //For non-voids:
- calculator.Add(-1, -1).Returns(x => { throw new Exception(); });
-
- //For voids and non-voids:
- calculator
- .When(x => x.Add(-2, -2))
- .Do(x => { throw new Exception(); });
-
- //Both calls will now throw.
- Assert.Throws<Exception>(() => calculator.Add(-1, -1));
- Assert.Throws<Exception>(() => calculator.Add(-2, -2));
事件
- public interface IEngine {
- event EventHandler Idling;
- event EventHandler<LowFuelWarningEventArgs> LowFuelWarning;
- event Action<int> RevvedAt;
- }
-
- public class LowFuelWarningEventArgs : EventArgs {
- public int PercentLeft { get; private set; }
- public LowFuelWarningEventArgs(int percentLeft) {
- PercentLeft = percentLeft;
- }
- }
- var wasCalled = false;
- //设置事件
- engine.Idling += (sender, args) => wasCalled = true;
- //执行事件
- //Tell the substitute to raise the event with a sender and EventArgs:
- engine.Idling += Raise.EventWith(new object(), new EventArgs());
-
- Assert.True(wasCalled);
事件参数
- engine.LowFuelWarning += (sender, args) => numberOfEvents++;
- //Raise.EventWith<TEventArgs>(...)
- //Raise event with specific args, any sender:
- engine.LowFuelWarning += Raise.EventWith(new LowFuelWarningEventArgs(10));
- //Raise event with specific args and sender:
- engine.LowFuelWarning += Raise.EventWith(new object(), new LowFuelWarningEventArgs(10));
-
- Assert.AreEqual(2, numberOfEvents);
代理事件
- var sub = Substitute.For<INotifyPropertyChanged>();
- bool wasCalled = false;
- sub.PropertyChanged += (sender, args) => wasCalled = true;
-
- sub.PropertyChanged += Raise.Event<PropertyChangedEventHandler>(this, new PropertyChangedEventArgs("test"));
-
- Assert.That(wasCalled);
action事件
- int revvedAt = 0;;
- engine.RevvedAt += rpm => revvedAt = rpm;
-
- engine.RevvedAt += Raise.Event<Action<int>>(123);
-
- Assert.AreEqual(123, revvedAt);
自动递归模拟
递归模拟
- public interface INumberParser {
- IEnumerable<int> Parse(string expression);
- }
- public interface INumberParserFactory {
- INumberParser Create(char delimiter);
- }
- var factory = Substitute.For<INumberParserFactory>();
- var parser = Substitute.For<INumberParser>();
- factory.Create(',').Returns(parser);
- parser.Parse("an expression").Returns(new[] {1,2,3})
- Assert.AreEqual(
- factory.Create(',').Parse("an expression"),
- new[] {1,2,3});
-
- //or
- var factory = Substitute.For<INumberParserFactory>();
- factory.Create(',').Parse("an expression").Returns(new[] {1,2,3});
- Assert.AreEqual(
- factory.Create(',').Parse("an expression"),
- new[] {1,2,3});
-
-
- var firstCall = factory.Create(',');
- var secondCall = factory.Create(',');
- var thirdCallWithDiffArg = factory.Create('x');
-
- Assert.AreSame(firstCall, secondCall);
- Assert.AreNotSame(firstCall, thirdCallWithDiffArg);
Substitute chains(链)
- public interface IContext {
- IRequest CurrentRequest { get; }
- }
- public interface IRequest {
- IIdentity Identity { get; }
- IIdentity NewIdentity(string name);
- }
- public interface IIdentity {
- string Name { get; }
- string[] Roles();
- }
- var context = Substitute.For<IContext>();
- context.CurrentRequest.Identity.Name.Returns("My pet fish Eric");
- Assert.AreEqual(
- "My pet fish Eric",
- context.CurrentRequest.Identity.Name);
自动值
- var identity = Substitute.For<IIdentity>();
- Assert.AreEqual(String.Empty, identity.Name);
- Assert.AreEqual(0, identity.Roles().Length);
设置out或者ref参数
- public interface ILookup {
- bool TryLookup(string key, out string value);
- }
- //Arrange
- var value = "";
- var lookup = Substitute.For<ILookup>();
- lookup
- .TryLookup("hello", out value)
- .Returns(x => {
- x[1] = "world!";
- return true;
- });
-
- //Act
- var result = lookup.TryLookup("hello", out value);
-
- //Assert
- Assert.True(result);
- Assert.AreEqual(value, "world!");
行为和参数匹配
执行回调
- public interface IOrderProcessor {
- void ProcessOrder(int orderId, Action<bool> orderProcessed);
- }
-
- public class OrderPlacedCommand {
- IOrderProcessor orderProcessor;
- IEvents events;
- public OrderPlacedCommand(IOrderProcessor orderProcessor, IEvents events) {
- this.orderProcessor = orderProcessor;
- this.events = events;
- }
- public void Execute(ICart cart) {
- orderProcessor.ProcessOrder(
- cart.OrderId,
- wasOk => { if (wasOk) events.RaiseOrderProcessed(cart.OrderId); }
- );
- }
- }
- [Test]
- public void Placing_order_should_raise_order_processed_when_processing_is_successful() {
- //Arrange
- var cart = Substitute.For<ICart>();
- var events = Substitute.For<IEvents>();
- var processor = Substitute.For<IOrderProcessor>();
- cart.OrderId = 3;
- //Arrange for processor to invoke the callback arg with `true` whenever processing order id 3
- processor.ProcessOrder(3, Arg.Invoke(true));
-
- //Act
- var command = new OrderPlacedCommand(processor, events);
- command.Execute(cart);
-
- //Assert
- events.Received().RaiseOrderProcessed(3);
- }
用参数执行操作
- var argumentUsed = 0;
- calculator.Multiply(Arg.Any<int>(), Arg.Do<int>(x => argumentUsed = x));
-
- calculator.Multiply(123, 42);
-
- Assert.AreEqual(42, argumentUsed);
- var firstArgsBeingMultiplied = new List<int>();
- calculator.Multiply(Arg.Do<int>(x => firstArgsBeingMultiplied.Add(x)), 10);
-
- calculator.Multiply(2, 10);
- calculator.Multiply(5, 10);
- calculator.Multiply(7, 4567); //Will not match our Arg.Do as second arg is not 10
-
- Assert.AreEqual(firstArgsBeingMultiplied, new[] { 2, 5 });
参数规范
- var numberOfCallsWhereFirstArgIsLessThan0 = 0;
- //Specify a call where the first arg is less than 0, and the second is any int.
- //When this specification is met we'll increment a counter in the Arg.Do action for
- //the second argument that was used for the call, and we'll also make it return 123.
- calculator
- .Multiply(
- Arg.Is<int>(x => x < 0),
- Arg.Do<int>(x => numberOfCallsWhereFirstArgIsLessThan0++)
- ).Returns(123);
-
- var results = new[] {
- calculator.Multiply(-4, 3),
- calculator.Multiply(-27, 88),
- calculator.Multiply(-7, 8),
- calculator.Multiply(123, 2) //First arg greater than 0, so spec won't be met.
- };
-
- Assert.AreEqual(3, numberOfCallsWhereFirstArgIsLessThan0); //3 of 4 calls have first arg < 0
- Assert.AreEqual(results, new[] {123, 123, 123, 0}); //Last call returns 0, not 123
检查调用命令
- [Test]
- public void TestCommandRunWhileConnectionIsOpen() {
- var connection = Substitute.For<IConnection>();
- var command = Substitute.For<ICommand>();
- var subject = new Controller(connection, command);
-
- subject.DoStuff();
-
- Received.InOrder(() => {
- connection.Open();
- command.Run(connection);
- connection.Close();
- });
- }
- [Test]
- public void SubscribeToEventBeforeOpeningConnection() {
- var connection = Substitute.For<IConnection>();
- connection.SomethingHappened += () => { /* some event handler */ };
- connection.Open();
-
- Received.InOrder(() => {
- connection.SomethingHappened += Arg.Any<Action>();
- connection.Open();
- });
- }
只要心中有梦,不管什么天气都适合睡觉