@@ -9,13 +9,16 @@ You should be able to test a service call even when the service is offline. `Ref
99lets your test choose the reply and inspect the request your client sends. You can check a
1010successful 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.
6871Read [ 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.
7276Give 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.
164218Use 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.
169223Creating 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