-
Notifications
You must be signed in to change notification settings - Fork 11
/
RequestExtensionsTests.cs
313 lines (262 loc) · 15.3 KB
/
RequestExtensionsTests.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Flurl;
using Xunit;
namespace Moq.Contrib.HttpClient.Test
{
public class RequestExtensionsTests
{
[Fact]
public async Task MatchesAnyRequest()
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient(); // Equivalent to `new HttpClient(handler.Object, false)`
// See ResponseExtensionsTests for response examples; this could be shortened to `.ReturnsResponse("foo")`
var response = new HttpResponseMessage()
{
Content = new StringContent("foo")
};
handler.SetupAnyRequest()
.ReturnsAsync(response);
// All requests made with HttpClient go through the handler's SendAsync() which we've mocked
(await client.GetAsync("http://localhost")).Should().BeSameAs(response);
(await client.PostAsync("https://example.com/foo", new StringContent("data"))).Should().BeSameAs(response);
(await client.GetStringAsync("https://example.com/bar")).Should().Be("foo");
// Verify methods are provided matching the setup helpers, although even without MockBehavior.Strict,
// HttpClient will throw if the request was not mocked, so in many cases a Verify will be redundant
handler.VerifyAnyRequest(Times.Exactly(3));
}
[Fact]
public async Task MatchesRequestByUrl()
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
// Here we're basing the language off the url, but we could also specify a setup that checks for example the
// Accept-Language header (see MatchesCustomPredicate below)
var enUrl = "https://example.com/en-US/hello";
var jaUrl = "https://example.com/ja-JP/hello";
// The helpers return the same Moq interface as the regular Setup, so we can use the standard Moq methods
// for more complex responses, in this case based on the request body
handler.SetupRequest(enUrl)
.Returns(async (HttpRequestMessage request, CancellationToken _) => new HttpResponseMessage()
{
Content = new StringContent($"Hello, {await request.Content.ReadAsStringAsync()}")
});
handler.SetupRequest(jaUrl)
.Returns(async (HttpRequestMessage request, CancellationToken _) => new HttpResponseMessage()
{
Content = new StringContent($"こんにちは、{await request.Content.ReadAsStringAsync()}")
});
// Imagine we have a service that returns a greeting for a given locale
async Task<string> GetGreeting(string locale, string name)
{
var response = await client.PostAsync($"https://example.com/{locale}/hello", new StringContent(name));
return await response.Content.ReadAsStringAsync();
}
// Call the "service" which we expect to make the requests set up above
string enGreeting = await GetGreeting("en-US", "world");
string jaGreeting = await GetGreeting("ja-JP", "世界");
enGreeting.Should().Be("Hello, world");
jaGreeting.Should().Be("こんにちは、世界"); // Konnichiwa, sekai
// The handler was created with MockBehavior.Strict which throws a MockException for invocations without setups
Func<Task> esAttempt = () => GetGreeting("es-ES", "mundo");
await esAttempt.Should().ThrowAsync<MockException>(because: "a setup for Spanish was not configured");
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("DELETE")]
public async Task MatchesRequestByMethod(string methodStr)
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
var method = new HttpMethod(methodStr); // Normally you'd use HttpMethod.Get, etc.
var url = "https://example.com";
var expected = $"This is {methodStr}!";
handler.SetupRequest(method, url)
.ReturnsResponse(expected);
var response = await client.SendAsync(new HttpRequestMessage(method, url));
var actual = await response.Content.ReadAsStringAsync();
actual.Should().Be(expected);
// Ensure this isn't simply matching any request
Func<Task> otherMethodAttempt = () => client.SendAsync(new HttpRequestMessage(HttpMethod.Options, url));
await otherMethodAttempt.Should().ThrowAsync<MockException>("the setup should not match an OPTIONS request");
}
[Fact]
public async Task MatchesCustomPredicate()
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
// Let's simulate posting a song to a music API
var url = new Uri("https://example.com/api/songs");
var token = "auth token obtained somehow";
var expected = new Song()
{
Title = "The Disease Called Love",
Artist = "Neru feat. Kagamine Rin, Kagamine Len",
Album = "CYNICISM",
Url = "https://youtu.be/2IH-toUoq3w"
};
// Set up a response for a request with this song
handler
.SetupRequest(HttpMethod.Post, url, async request =>
{
// Here we can parse the request json. Anything you would check with It.Is() should go here. Tip: If
// Song were a record type, we could compare the entire object at once with `return json == expected`
var json = await request.Content.ReadFromJsonAsync<Song>();
return json.Title == expected.Title /* ... */;
})
.ReturnsResponse(HttpStatusCode.Created);
// Or, if you know the code under test is using the System.Text.Json extensions, you can skip
// deserialization and access the original class directly:
//
// .SetupRequest(HttpMethod.Post, url, async r => ((JsonContent)r.Content).Value == expected)
//
// Alternatively, to do asserts on the sent model (like a db insert), use Callback to save the model,
// and then Verify it was only called once. (You can also just put asserts in the match predicate!)
//
// .Callback((HttpRequestMessage request, CancellationToken _) =>
// {
// actual = request.Content.ReadFromJsonAsync<Song>().Result;
// });
// A request without a valid auth token should fail (the last setup takes precedence)
handler.SetupRequest(r => r.Headers.Authorization?.Parameter != token)
.ReturnsResponse(HttpStatusCode.Unauthorized);
// Imaginary service method that calls the API we're mocking
async Task CreateSong(Song song, string authToken)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
var response = await client.PostAsJsonAsync(url, song);
response.EnsureSuccessStatusCode();
}
// Create the song
await CreateSong(expected, token);
// The setup won't match if the request json contains a different song
Func<Task> wrongSongAttempt = () => CreateSong(new Song()
{
Title = "Plug Out (HSP 2012 Remix)",
Artist = "鼻そうめんP feat. 初音ミク",
Album = "Hiroyuki ODA pres. HSP WORKS 11-14",
Url = "https://vocadb.net/S/21567"
}, token);
await wrongSongAttempt.Should().ThrowAsync<MockException>("wrong request body");
// Attempt to create the song again, this time without a valid token (if we were actually testing a service,
// this would probably be a separate unit test)
Func<Task> unauthorizedAttempt = () => CreateSong(expected, "expired token");
await unauthorizedAttempt.Should().ThrowAsync<HttpRequestException>("this should 400, causing EnsureSuccessStatusCode() to throw");
}
[Fact]
public async Task MatchesQueryParameters()
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
string baseUrl = "https://example.com:8080/api/v2";
// A URL builder like Flurl can make dealing with query params easier (https://flurl.io/docs/fluent-url/)
handler.SetupRequest(baseUrl.AppendPathSegment("search").SetQueryParam("q", "fus ro dah"))
.ReturnsResponse(HttpStatusCode.OK);
// Note that the above is still matching an exact url; it's often better instead to use a predicate like so,
// since params may come in different orders and we may not need to check all of them either
handler
.SetupRequest(HttpMethod.Post, r =>
{
Url url = r.RequestUri; // Implicit conversion from Uri to Url
return url.Path == baseUrl.AppendPathSegment("followers/enlist") &&
url.QueryParams["name"].Equals("Lydia");
})
.ReturnsResponse(HttpStatusCode.OK);
await client.GetAsync("https://example.com:8080/api/v2/search?q=fus%20ro%20dah");
// In this example we've passed an additional query param that the test doesn't care about
await client.PostAsync("https://example.com:8080/api/v2/followers/enlist?name=Lydia&carryBurdens=yes", null);
// Verifying just to show that both setups were invoked, rather than one for both requests (HttpClient would
// have thrown already if none had matched for a request)
handler.VerifyAll();
}
// This next test ensures that methods that close the request stream, such as ReadFromJsonAsync, can safely be
// used in the match predicate. Moq did not intend for matchers to have side effects; its code sometimes calls
// the matcher a second time, which would cause these methods to throw as the content had already been consumed:
//
// https://github.com/moq/moq4/blob/v4.18.1/src/Moq/Match.cs#L182 (when Matches returns true)
// https://github.com/moq/moq4/blob/v4.8.0/Source/SetupCollection.cs#L100-L112 (when Matches returns false)
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task MatchPredicateRunsOnlyOnce(bool shouldMatch)
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
var predicate = new Mock<Func<HttpRequestMessage, Task<bool>>>();
// Have the match predicate consume the request content before returning true. This will close the stream,
// which means a second invocation with the same request will throw. Having this be a mock allows us to
// directly Verify how many times it was called as a second line of defense in case ReadFromJsonAsync's
// behavior changes.
predicate.Setup(x => x(It.IsAny<HttpRequestMessage>()))
.Returns(async (HttpRequestMessage request) =>
{
await request.Content.ReadFromJsonAsync<string>();
return shouldMatch;
});
// We'll send two requests, to ensure that the predicate is called once _per request_, not once per setup
handler.SetupRequest(predicate.Object)
.ReturnsResponse(HttpStatusCode.OK);
var first = new Uri("https://example.com/first");
try
{
await client.PostAsJsonAsync(first, "");
} catch (MockException) { }
var second = new Uri("https://example.com/second");
try
{
await client.PostAsJsonAsync(second, "");
}
catch (MockException) { }
// Verify
predicate.Verify(x => x(It.IsAny<HttpRequestMessage>()), Times.Exactly(2));
predicate.Verify(x => x(It.Is<HttpRequestMessage>(r => r.RequestUri == first)), Times.Once());
predicate.Verify(x => x(It.Is<HttpRequestMessage>(r => r.RequestUri == second)), Times.Once());
}
[Fact]
public async Task VerifyHelpersThrowAsExpected() // This one is mainly for code coverage
{
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
// Mock two different endpoints
string fooUrl = "https://example.com/foo";
string barUrl = "https://example.com/bar";
handler.SetupRequest(fooUrl)
.ReturnsAsync(new HttpResponseMessage());
handler.SetupRequest(barUrl)
.ReturnsAsync(new HttpResponseMessage());
// Make various calls
await client.GetAsync(fooUrl);
await client.PostAsync(fooUrl, new StringContent("stuff"));
await client.GetAsync(barUrl);
// Prepare verify attempts using various overloads
Action verifyThreeRequests = () => handler.VerifyAnyRequest(Times.Exactly(3));
Action verifyMoreThanThreeRequests = () => handler.VerifyAnyRequest(Times.AtLeast(4), "oh noes");
Action verifyTwoFoos = () => handler.VerifyRequest(fooUrl, Times.Exactly(2));
Action verifyThreeFoos = () => handler.VerifyRequest(fooUrl, Times.Exactly(3), "oh noes");
Action verifyFooPosted = () => handler.VerifyRequest(HttpMethod.Post, fooUrl);
Action verifyBarPosted = () => handler.VerifyRequest(HttpMethod.Post, barUrl, failMessage: "oh noes");
// Assert that these pass or fail accordingly
verifyThreeRequests.Should().NotThrow("we made three requests");
verifyMoreThanThreeRequests.Should().Throw<MockException>("we only made three requests");
verifyTwoFoos.Should().NotThrow("there were two requests to foo");
verifyThreeFoos.Should().Throw<MockException>("there were two requests to foo, not three");
verifyFooPosted.Should().NotThrow("we sent a POST to foo");
verifyBarPosted.Should().Throw<MockException>("we did not send a POST to bar");
// The fail messages should be passed along as well
var messages = new[] { verifyMoreThanThreeRequests, verifyThreeFoos, verifyBarPosted }
.Select(f => { try { f(); return null; } catch (MockException ex) { return ex.Message; } });
messages.Should().OnlyContain(x => x.Contains("oh noes"), "all verify exceptions should contain the failMessage");
}
}
}