Skip to content

Commit bb7da01

Browse files
committed
docs(refit): rewrite the testing topic pages around the Refit examples
The seven Refit.Testing topic pages (index, routes, replies, verification, faults, response-stubs, streaming) now excerpt the final samples from src/examples/Documentation/Testing/ in reactiveui/refit#2338 verbatim, up to each sample's documentation marker. - Pages are framework-neutral: Refit.Testing code with results as comments, no test-framework attributes or asserts. Full tests per framework are on the xunit, nunit, mstest and tunit pages. - Each section states the problem the sample solves, shows it, then explains what it shows, when to use it and what to watch out for. - Document the cancellable Reply.From overload, RequestCapture's Full, None and Bounded modes, NetworkBehavior's defaults and seed, and how a transport failure surfaces through a Refit client. - Describe current Refit.Testing behaviour that surprises readers: the duplicate exact-query match, verification after adding a route, the one-shot race and the UTF-8 re-read of captured bodies.
1 parent db2dae6 commit bb7da01

7 files changed

Lines changed: 1152 additions & 686 deletions

File tree

‎docs/documentation/refit/testing/faults.md‎

Lines changed: 145 additions & 110 deletions
Large diffs are not rendered by default.

‎docs/documentation/refit/testing/index.md‎

Lines changed: 100 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ You should be able to test a service call even when the service is offline. `Ref
99
lets your test choose the reply and inspect the request your client sends. You can check a
1010
successful call, an error or missing data without relying on a running server.
1111

12-
The main helper, `StubHttp`, stands in for the HTTP handler. Your real Refit client still
13-
builds the request, so the test can catch mistakes in its route, headers and body.
12+
The main helper, `StubHttp`, stands in for the HTTP handler. A handler is the object that `HttpClient`
13+
passes each request to. `StubHttp` is a stub: a stand-in that answers with replies you set up and never
14+
touches the network. Your real Refit client still builds the request, so the test can catch mistakes in
15+
its route, headers and body.
1416

1517
## Pick your test framework
1618

17-
`Refit.Testing` works with any test framework. The same set of tests exists for each of the four main ones.
18-
The tests differ only in their attributes and assert calls, so pick the page for the framework your project uses:
19+
`Refit.Testing` works with any test framework. It has no test attributes or assert methods of its own.
20+
The samples on these pages note each result in a comment. To see the same checks written as full tests,
21+
pick the page for the framework your project uses:
1922

2023
- [xUnit](xunit.md)
2124
- [NUnit](nunit.md)
@@ -27,9 +30,9 @@ and streaming data in and out.
2730

2831
## Make your first test
2932

30-
**1. Reference `Refit.Testing` and Refit.** The complete [runnable .NET 10 example](https://github.com/reactiveui/refit/blob/main/src/examples/Documentation/Testing/Testing.cs)
31-
use the packages' source projects. An app can reference the corresponding NuGet packages.
32-
The sample needs no server and no internet connection when it runs.
33+
**1. Reference `Refit.Testing` and Refit.** Add both NuGet packages to your test project.
34+
The runnable .NET 10 example, [`Testing.cs`](https://github.com/reactiveui/refit/blob/main/src/examples/Documentation/Testing/Testing.cs),
35+
references their source projects instead. It needs no server and no internet connection when it runs.
3336

3437
**2. Declare a model and interface.** The model is the C# shape of the reply body.
3538

@@ -68,65 +71,116 @@ Native AOT needs this generated metadata for the request and reply models.
6871
Read [JSON configuration](../serialization/json.md) and [AOT](../aot.md) for registration limits.
6972

7073

71-
Put these two members in your test class. `CreateSettings()` returns new settings each time.
74+
**4. Add the settings members.** Put these two members in your test class.
75+
`CreateSettings()` returns new settings each time.
7276
Give each handler its own settings, because `StubHttp` changes the settings you pass it.
7377

74-
7578
```csharp
76-
private static readonly JsonSerializerOptions JsonOptions = new(TestingJsonContext.Default.Options) { TypeInfoResolver = TestingJsonContext.Default };
79+
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions(TestingJsonContext.Default.Options) { TypeInfoResolver = TestingJsonContext.Default };
7780

78-
private static RefitSettings CreateSettings() => new(new SystemTextJsonContentSerializer(JsonOptions));
81+
private static RefitSettings CreateSettings() => new RefitSettings(new SystemTextJsonContentSerializer(JsonOptions));
7982
```
8083

81-
**4. Choose replies and make real client calls.** A route selects a request by its method and path.
82-
A one-shot expectation must receive one matching request. Each entry below is such an expectation.
83-
`VerifyAllCalled()` fails the test when an expectation received no request.
84-
The examples use xUnit's `Assert`; use your test framework's equivalent.
84+
**5. Stub the calls and send them.** You want to send a real Refit request with no server, then see
85+
what your client sent. Fill a `StubHttp` with entries. Each entry pairs a route with a reply.
86+
A route is a rule that picks which requests the entry answers, here by HTTP method and path.
87+
A reply is what the stub sends back. By default a route is one-shot: it answers one matching request,
88+
then it is used up.
8589

90+
[//]: # "excerpt:Testing/Testing.cs#ShowClientAsync"
8691

8792
```csharp
88-
[Fact]
89-
public async Task GetAsync_ReturnsThePerson()
93+
using StubHttp http = new StubHttp
9094
{
91-
// Arrange
92-
using StubHttp http = new()
9395
{
94-
{ Route.Get("/people/{id}"), Reply.With(new TestingPerson(1, "Ada")) },
95-
};
96-
ITestingApi api = http.CreateGeneratedClient<ITestingApi>("https://api.example.com", CreateSettings());
96+
Route.Get("/people/{id}"),
97+
Reply.With(new TestingPerson(1, "Ada"))
98+
},
99+
{
100+
Route.Post("/people"),
101+
Reply.With(new TestingPerson(2, "Grace"), HttpStatusCode.Created)
102+
},
103+
};
104+
ITestingApi api = http.CreateGeneratedClient<ITestingApi>("https://api.example.com", CreateSettings());
105+
106+
TestingPerson person = await api.GetAsync(1); // person.Name == "Ada"
107+
TestingPerson created = await api.CreateAsync(new TestingPerson(2, "Grace")); // created.Name == "Grace"
108+
TestingPerson? sent = await http.LastRequestBodyAsync<TestingPerson>(); // sent?.Name == "Grace"
109+
```
97110

98-
// Act
99-
TestingPerson person = await api.GetAsync(1);
111+
`Reply.With` turns a model into a JSON body. It uses the serializer from the settings you passed to
112+
`CreateGeneratedClient`. The client calls go through the stub, so Refit builds real requests.
100113

101-
// Assert
102-
Assert.Equal("Ada", person.Name);
103-
http.VerifyAllCalled();
104-
}
114+
The stub also captures each request. To capture a request is to keep a copy of its body, so you can read
115+
it after the call ends. `LastRequestBodyAsync<T>()` reads the latest captured body back as a model.
116+
`RequestBodyAsync<T>(index)` reads the body at a zero-based position in `Requests`.
117+
118+
Finish each test with `http.VerifyAllCalled()`. It throws when a one-shot route received no request.
119+
[Verification](verification.md) covers it in full. Watch out for two rules:
120+
121+
- A request that no route matches throws `InvalidOperationException`. The stub does not return an automatic 404.
122+
- A one-shot route answers once. A second `GetAsync(1)` here would find no route. Add another entry, or make
123+
the route reusable as [Routes](routes.md) shows.
124+
125+
The page's code needs `using` directives for `System.Net`, `System.Text.Json`, `System.Text.Json.Serialization`,
126+
`Refit` and `Refit.Testing`.
127+
128+
## Point settings at the stub
129+
130+
Some code takes a `RefitSettings` and builds its own client. You want that code to talk to the stub instead
131+
of the real network. The [`Testing.cs`](https://github.com/reactiveui/refit/blob/main/src/examples/Documentation/Testing/Testing.cs)
132+
sample shows the three ways to wire it up.
133+
134+
[//]: # "excerpt:Testing/Testing.cs#ShowSettingsWiringAsync"
135+
136+
```csharp
137+
using StubHttp http = new StubHttp();
138+
RefitSettings fresh = http.ToSettings(); // fresh.HttpMessageHandlerFactory!() == http
139+
RefitSettings supplied = CreateSettings();
140+
RefitSettings wired = http.ToSettings(supplied); // wired is the same instance as supplied
141+
142+
// Uses default settings, which replaces the serializer wired in above: the stub keeps one, and the last call wins.
143+
ITestingApi api = http.CreateGeneratedClient<ITestingApi>("https://api.example.com");
105144
```
106145

107-
**5. Check what the client sent.** The handler records each request. `LastRequestBodyAsync<T>()` reads the latest
108-
body back as a model. `RequestBodyAsync<T>(index)` reads the body at a zero-based position in `Requests`.
109-
The rest of the examples on these pages show only the body of a test.
146+
- `ToSettings()` creates new settings. Their `HttpMessageHandlerFactory` returns the stub.
147+
- `ToSettings(supplied)` changes your settings in place and returns the same instance.
148+
- `CreateGeneratedClient<T>(hostUrl)` with no settings creates default settings and wires them the same way.
149+
150+
Each of these calls also makes the stub adopt the settings' serializer. The stub holds one serializer, and
151+
the last call wins. In the sample, the last call uses default settings. The stub now writes typed replies and
152+
reads captured bodies with the default serializer, not the one from `CreateSettings()`.
153+
Wire one set of settings to each stub.
154+
155+
## Use a reflection client
110156

157+
Your interface may have no generated client, for example in a project without Refit's source generator.
158+
`CreateClient<T>` builds the client at run time with reflection instead. Reflection means reading type
159+
information while the app runs. The sample is in
160+
[`TestingReflection.cs`](https://github.com/reactiveui/refit/blob/main/src/examples/Documentation/Testing/TestingReflection.cs).
161+
162+
[//]: # "excerpt:Testing/TestingReflection.cs#RunAsync"
111163

112164
```csharp
113-
using StubHttp http = new()
114-
{
115-
{ Route.Post("/people"), Reply.With(new TestingPerson(2, "Grace"), HttpStatusCode.Created) },
116-
};
117-
ITestingApi api = http.CreateGeneratedClient<ITestingApi>("https://api.example.com", CreateSettings());
165+
using StubHttp http = new StubHttp { { Route.Get("/people/1"), Reply.Json("{\"id\":1,\"name\":\"Ada\"}") } };
166+
ITestingApi defaults = http.CreateClient<ITestingApi>("https://api.example.com");
167+
TestingPerson first = await defaults.GetAsync(1); // first.Name == "Ada"
118168
119-
_ = await api.CreateAsync(new TestingPerson(2, "Grace"));
169+
RefitSettings settings = new RefitSettings(new SystemTextJsonContentSerializer(TestingJsonContext.Default.Options));
120170

121-
TestingPerson? sent = await http.LastRequestBodyAsync<TestingPerson>();
122-
Assert.Equal("Grace", sent?.Name);
123-
Assert.Equal(sent, await http.RequestBodyAsync<TestingPerson>(0)); // the same body, read by its index
124-
Assert.Equal(HttpMethod.Post, http.Requests[0].Method);
125-
http.VerifyAllCalled();
171+
// each route answers once, so add another for the second call
172+
http.Add(Route.Get("/people/1"), Reply.Json("{\"id\":1,\"name\":\"Ada\"}"));
173+
ITestingApi configured = http.CreateClient<ITestingApi>("https://api.example.com", settings);
174+
TestingPerson second = await configured.GetAsync(1); // second == first
126175
```
127176

128-
Add `using` directives for `System.Net`, `System.Text.Json`, `System.Text.Json.Serialization`, `Refit`,
129-
`Refit.Testing` and `Xunit`.
177+
`CreateClient<T>(hostUrl)` uses default settings. `CreateClient<T>(hostUrl, settings)` keeps your serializer.
178+
`Reply.Json` sends raw JSON text, so the reply itself needs no model metadata.
179+
The route is one-shot, so the sample adds a second entry before the second call.
180+
181+
Both overloads call `RestService.For<T>`. On modern targets they carry a trimming warning. Trimming removes
182+
code the build believes is unused, and reflection can need that code. The example's Native AOT host leaves
183+
this sample out. Prefer `CreateGeneratedClient<T>` for new tests.
130184

131185
## Choose the test boundary
132186

@@ -164,42 +218,10 @@ They throw `InvalidOperationException` when none is registered.
164218
Use the settings overload and generated JSON context in trimmed or AOT apps.
165219

166220
`CreateClient<T>(hostUrl)` and `CreateClient<T>(hostUrl, baseSettings)` use `RestService.For<T>`.
167-
That path permits runtime reflection and carries a trimming warning on modern targets.
168-
Trimming removes code the build believes is unused. Prefer the generated factories for new tests.
221+
That path uses reflection and carries a trimming warning on modern targets.
222+
[Use a reflection client](#use-a-reflection-client) shows both. Prefer the generated factories for new tests.
169223
Creating a client does not make an HTTP request or prove that its JSON configuration is complete.
170224

171-
172-
This test shows that `ToSettings(settings)` returns your instance, now pointed at the handler.
173-
174-
175-
```csharp
176-
using StubHttp http = new();
177-
RefitSettings settings = CreateSettings();
178-
179-
RefitSettings returned = http.ToSettings(settings);
180-
181-
Assert.Same(settings, returned);
182-
Assert.Same(http, settings.HttpMessageHandlerFactory!());
183-
```
184-
185-
The reflection-based factories run in the JIT version of the example. The native host excludes them.
186-
With generated JSON metadata in the settings, the client factory itself can still fall back to reflection.
187-
`CreateClient<T>(hostUrl)` works the same way with default settings.
188-
Use generated factories for native execution.
189-
190-
191-
```csharp
192-
using StubHttp http = new()
193-
{
194-
{ Route.Get("/people/1"), Reply.Json("""{"id":1,"name":"Ada"}""") },
195-
};
196-
ITestingApi api = http.CreateClient<ITestingApi>("https://api.example.com", CreateSettings());
197-
198-
TestingPerson person = await api.GetAsync(1);
199-
200-
Assert.Equal("Ada", person.Name);
201-
```
202-
203225
## API reference
204226

205227
| API | Description | Parameters | Returns and behavior |

0 commit comments

Comments
 (0)