Skip to content

Commit 5e83393

Browse files
committed
feat: Add NSubstitute documentation covering skill usage, core API, async handling, project patterns, and common mistakes
1 parent 92e971c commit 5e83393

4 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: nsubstitute
3+
description: Use NSubstitute to create test doubles (mocks/stubs/spies) for .NET interfaces and classes, covering Substitute.For<T>, Returns/ReturnsForAnyArgs, Received/DidNotReceive, Arg matchers, async Task stubbing, callbacks, and partial substitutes. Always trigger when the user writes, reviews, or asks about mocking, faking dependencies, Substitute.For, Received(), DidNotReceive(), Arg.Any, Arg.Is, test doubles, verifying interactions, setting up shared mock infrastructure, or stubbing async methods in .NET tests — even if they don't mention NSubstitute by name. Prefer this skill over guessing; NSubstitute's argument matcher rules, the discard pattern for Returns(), async Task<T> type inference, nested interface substitution (e.g., Marten's IDocumentSession.Events), and partial substitute pitfalls all have non-obvious failure modes that are easy to get wrong.
4+
---
5+
6+
# NSubstitute Skill
7+
8+
NSubstitute (v5) is the mocking library used in this project. It creates in-memory test doubles for interfaces (and optionally classes) with a fluent, readable API.
9+
10+
## Quick-start anatomy
11+
12+
```csharp
13+
using NSubstitute;
14+
15+
// 1. Create — always prefer interfaces over classes
16+
var emailService = Substitute.For<IEmailService>();
17+
18+
// 2. Stub — discard result with _ = to satisfy the compiler
19+
_ = emailService.SendAsync(Arg.Any<string>()).Returns(Task.CompletedTask);
20+
21+
// 3. Execute the system under test
22+
await handler.Handle(command);
23+
24+
// 4. Assert interactions
25+
await emailService.Received(1).SendAsync("user@example.com");
26+
await emailService.DidNotReceive().SendAsync(Arg.Is<string>(s => s.Contains("admin")));
27+
```
28+
29+
Key rules at a glance:
30+
- **Discard return of `.Returns()`**: assign to `_ =` to avoid compiler warnings.
31+
- **Never use arg matchers in real calls**: only in `.Returns(...)`, `.Received()`, or `.When(...).Do(...)`.
32+
- **Await async `.Received()` calls**: NSubstitute tracks `async Task` calls — the assertion itself is synchronous, but the method signature needs `await` to compile.
33+
34+
---
35+
36+
## Reference files — read before writing code
37+
38+
| Topic | File |
39+
|-------|------|
40+
| Creating substitutes, Returns, Received, Arg matchers | [references/api.md](references/api.md) |
41+
| Async, exceptions, callbacks, events, partial subs | [references/async-and-advanced.md](references/async-and-advanced.md) |
42+
| BookStore project patterns (HandlerTestBase, nested interfaces) | [references/project-patterns.md](references/project-patterns.md) |
43+
44+
---
45+
46+
## Common mistakes
47+
48+
```
49+
✅ _ = sub.Method().Returns(value) ❌ sub.Method().Returns(value) // compiler warning
50+
✅ sub.Received().Method(Arg.Any<T>()) ❌ sub.Method(Arg.Any<T>()) // matcher in real call
51+
✅ Substitute.For<IService>() ❌ Substitute.For<ConcreteClass>() // non-virtual not intercepted
52+
✅ sub.Method().Returns<string>(x => ...) ❌ sub.Method().Returns(x => ...) // CS0121 on Task<T>
53+
✅ await emailSvc.Received(1).SendAsync() ❌ emailSvc.Received().SendAsync() // forgetting await
54+
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# NSubstitute Core API Reference
2+
3+
## Creating substitutes
4+
5+
```csharp
6+
// Interface (recommended)
7+
var service = Substitute.For<IEmailService>();
8+
9+
// Multiple interfaces
10+
var cmd = Substitute.For<ICommand, IDisposable>();
11+
12+
// Class (only virtual members are intercepted — use with care)
13+
var reader = Substitute.For<SomReader>("ctor-arg");
14+
15+
// Delegate
16+
var transform = Substitute.For<Func<string, int>>();
17+
transform("hello").Returns(42);
18+
19+
// Partial substitute (real methods run unless configured)
20+
var partial = Substitute.ForPartsOf<MyClass>();
21+
partial.Configure().SomeVirtual().Returns("stubbed"); // use Configure() to avoid running real code
22+
```
23+
24+
The `Substitute.ForPartsOf<T>()` trap: calling `partial.SomeVirtual().Returns(...)` without `Configure()` **actually invokes the real method** during setup, causing side effects. Always use `partial.Configure().Method()` first.
25+
26+
---
27+
28+
## Setting return values
29+
30+
```csharp
31+
// Fixed value
32+
_ = service.GetUserAsync(userId).Returns(user);
33+
34+
// Value based on arguments (CallInfo)
35+
_ = service.GetAsync(default).ReturnsForAnyArgs(x => new User { Id = x.Arg<Guid>() });
36+
37+
// Sequence of values (each call gets the next)
38+
_ = cache.Get<string>("key").Returns("first", "second", "third");
39+
40+
// Sequence with exceptions
41+
_ = calculator.Mode.Returns("DEC", "HEX", x => { throw new Exception("exhausted"); });
42+
43+
// Property
44+
_ = httpContextAccessor.HttpContext.Returns(new DefaultHttpContext());
45+
```
46+
47+
`Returns(value)` is argument-specific: it only matches calls with those exact arguments. Use `ReturnsForAnyArgs(value)` (or `Arg.Any<T>()` matchers) when you don't care which arguments are passed.
48+
49+
---
50+
51+
## Argument matchers
52+
53+
Matchers are valid **only inside Return/Received/When.Do** calls — never in real production calls.
54+
55+
```csharp
56+
// Match anything
57+
service.Send(Arg.Any<string>()).Returns(true);
58+
59+
// Match with predicate
60+
service.Send(Arg.Is<string>(s => s.StartsWith("admin"))).Returns(false);
61+
62+
// Capture argument for inspection
63+
var captured = default(string);
64+
service.Send(Arg.Do<string>(x => captured = x)).Returns(true);
65+
66+
// out parameters
67+
lookup.TryLookup("key", out Arg.Any<string>())
68+
.Returns(x => { x[1] = "value"; return true; });
69+
```
70+
71+
---
72+
73+
## Verifying calls
74+
75+
```csharp
76+
// Received exactly N times (default = once or more if no arg given)
77+
service.Received(1).Send("hello@example.com");
78+
79+
// Received any number of times
80+
service.Received().Send(Arg.Any<string>());
81+
82+
// Not received
83+
service.DidNotReceive().Send("admin@example.com");
84+
85+
// Received with any args (ignores argument values entirely)
86+
service.ReceivedWithAnyArgs().Send(default);
87+
88+
// Property getter / setter
89+
_ = cache.Received().Get<User>("key");
90+
cache.Received().Size = Arg.Is<int>(n => n > 0);
91+
92+
// Indexers
93+
dictionary.Received()["key"] = Arg.Is<int>(v => v > 0);
94+
```
95+
96+
The `Received()` chain returns a proxy — access the member on it to declare what you expected. This is purely declarative; the framework inspects the recorded calls and throws `ReceivedCallsException` if the expectation isn't met.
97+
98+
---
99+
100+
## Clearing and resetting
101+
102+
```csharp
103+
// Clear recorded calls (but keep stubs)
104+
sub.ClearReceivedCalls();
105+
106+
// Clear both calls and stubs
107+
sub.ClearSubstitute(ClearOptions.All);
108+
```
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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

Comments
 (0)