|
| 1 | +# NSubstitute — Async, Exceptions, Callbacks, Events |
| 2 | + |
| 3 | +## Async methods (Task / Task<T> / ValueTask) |
| 4 | + |
| 5 | +NSubstitute handles async methods transparently — `.Returns()` accepts both synchronous values and Tasks. |
| 6 | + |
| 7 | +```csharp |
| 8 | +// Stub returning a value |
| 9 | +_ = service.GetUserAsync(id).Returns(user); // auto-wrapped in Task |
| 10 | +
|
| 11 | +// Stub returning Task.CompletedTask (void async) |
| 12 | +_ = service.SendAsync(email).Returns(Task.CompletedTask); |
| 13 | + |
| 14 | +// Stub returning null (explicit generic needed to avoid CS0121) |
| 15 | +_ = service.FindAsync(id).Returns((User?)null); |
| 16 | + |
| 17 | +// Throw inside a Returns callback — must specify the type explicitly (CS0121 workaround) |
| 18 | +_ = service.GetAsync(id).Returns<User>(x => { throw new NotFoundException(); }); |
| 19 | + |
| 20 | +// Verify async call |
| 21 | +await service.Received(1).GetUserAsync(id); |
| 22 | +``` |
| 23 | + |
| 24 | +**CS0121 tip**: When a method returns `Task<T>` and you pass a lambda that throws, the compiler can't resolve which `Returns` overload to use. Write `Returns<T>(x => { throw ... })` with the explicit generic parameter. |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Throwing exceptions |
| 29 | + |
| 30 | +```csharp |
| 31 | +// Synchronous throw on method call |
| 32 | +service.When(x => x.Process(Arg.Any<Order>())) |
| 33 | + .Do(x => { throw new InvalidOperationException("failed"); }); |
| 34 | + |
| 35 | +// Or using Throws extension (requires NSubstitute.Extensions or direct When/Do) |
| 36 | +service.When(x => x.Delete(Arg.Any<Guid>())) |
| 37 | + .Throw(new UnauthorizedException()); |
| 38 | + |
| 39 | +// Throw on async method |
| 40 | +_ = service.GetAsync(id).Returns<User>(_ => throw new NotFoundException()); |
| 41 | +``` |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Callbacks and side effects |
| 46 | + |
| 47 | +Use callbacks when you need side effects beyond a return value — for example, capturing arguments, simulating state mutations, or tracking invocation counts. |
| 48 | + |
| 49 | +```csharp |
| 50 | +// Capture argument via Arg.Do |
| 51 | +var sentEmails = new List<string>(); |
| 52 | +_ = emailService.SendAsync(Arg.Do<string>(addr => sentEmails.Add(addr))) |
| 53 | + .Returns(Task.CompletedTask); |
| 54 | + |
| 55 | +// Run code on every call using When...Do (good for void methods) |
| 56 | +var callCount = 0; |
| 57 | +service.When(x => x.Notify(Arg.Any<string>())) |
| 58 | + .Do(x => callCount++); |
| 59 | + |
| 60 | +// AndDoes — side effect alongside a return value |
| 61 | +_ = service.Process(Arg.Any<Order>()) |
| 62 | + .Returns(Result.Ok()) |
| 63 | + .AndDoes(x => Console.WriteLine($"Processed {x.Arg<Order>().Id}")); |
| 64 | + |
| 65 | +// Callback builder for sequenced behaviour |
| 66 | +service.When(x => x.Run()) |
| 67 | + .Do(Callback |
| 68 | + .First(x => Console.WriteLine("first call")) |
| 69 | + .Then(x => Console.WriteLine("second call")) |
| 70 | + .ThenKeepDoing(x => Console.WriteLine("subsequent"))); |
| 71 | +``` |
| 72 | + |
| 73 | +**When to use `When...Do` vs `Returns + AndDoes`**: prefer `Returns + AndDoes` for non-void methods (cleaner). Use `When...Do` for `void` methods or when you need complex sequenced behaviour via `Callback`. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## Events |
| 78 | + |
| 79 | +```csharp |
| 80 | +// Subscribe and raise event |
| 81 | +var command = Substitute.For<ICommand>(); |
| 82 | +command.Executed += Raise.Event(); // EventHandler |
| 83 | +command.DataReceived += Raise.Event<DataEventArgs>(args); // EventHandler<T> |
| 84 | +
|
| 85 | +// Verify subscription |
| 86 | +command.Received().Executed += Arg.Any<EventHandler>(); |
| 87 | +``` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Partial substitutes |
| 92 | + |
| 93 | +Use `ForPartsOf<T>` when you want real behaviour for most methods but need to stub one dependency-heavy or side-effectful method. |
| 94 | + |
| 95 | +```csharp |
| 96 | +var reader = Substitute.ForPartsOf<FileReader>(); |
| 97 | + |
| 98 | +// MUST call Configure() first — otherwise the real method runs during setup |
| 99 | +reader.Configure().ReadFile("data.csv").Returns("1,2,3"); |
| 100 | + |
| 101 | +// Now the real Read() will call the stubbed ReadFile() |
| 102 | +var result = reader.Read("data.csv"); |
| 103 | +``` |
| 104 | + |
| 105 | +Only works for `virtual` methods. Non-virtual code always runs as-is; use NSubstitute.Analyzers to catch this at compile time. |
0 commit comments