Skip to content

Commit d333774

Browse files
committed
Added Various Helpers section to the tutorial (#132)
1 parent e2050d8 commit d333774

File tree

8 files changed

+579
-29
lines changed

8 files changed

+579
-29
lines changed

docs/_docfx/tutorial/helpers.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Various Helpers
2+
3+
This section will cover various test helpers which make writing tests faster and easier! :)
4+
5+
## The Helpers package
6+
7+
You may add the **"MyTested.AspNetCore.Mvc.Helpers"** package which adds extension methods to the fluent API. But let's do something else. I believe you got tired of all these packages so we will delete them and add only the one that rules them all:
8+
9+
```json
10+
"dependencies": {
11+
"dotnet-test-xunit": "2.2.0-*",
12+
"xunit": "2.2.0-*",
13+
"Moq": "4.6.38-*",
14+
"MyTested.AspNetCore.Mvc.Universe": "1.0.0", // <---
15+
"MusicStore": "*"
16+
},
17+
```
18+
19+
The **"Universe"** package combines all other packages.
20+
21+
One of the helpers is allowing us to assert controller action results with a single method call instead of multiple ones. For example we may have these lines of code:
22+
23+
```c#
24+
.ShouldReturn()
25+
.View()
26+
.WithModel(model);
27+
```
28+
29+
They can be written like this:
30+
31+
```c#
32+
.ShouldReturn()
33+
.View(model)
34+
```
35+
36+
We have such test in **"AccountControllerTest"**:
37+
38+
```c#
39+
[Fact]
40+
public void LoginShouldReturnViewWithSameModelWithInvalidLoginViewModel()
41+
{
42+
var model = new LoginViewModel
43+
{
44+
Email = "[email protected]",
45+
Password = "invalid"
46+
};
47+
48+
var redirectUrl = "/Test/Url";
49+
50+
MyController<AccountController>
51+
.Instance()
52+
.Calling(c => c.Login(model, redirectUrl))
53+
.ShouldHave()
54+
.ModelState(modelState => modelState
55+
.ContainingError(string.Empty)
56+
.ThatEquals("Invalid login attempt."))
57+
.AndAlso()
58+
.ShouldReturn()
59+
.View(model); // <---
60+
}
61+
```
62+
63+
## Global MyMvc class
64+
65+
The **"MyTested.AspNetCore.Mvc"** package introduced another static class named **"MyMvc"**. Instead of these:
66+
67+
```c#
68+
MyController<TestController>.Instance()
69+
MyViewComponent<TestViewComponent>.Instance()
70+
MyRouting.Configuration()
71+
```
72+
73+
You can use:
74+
75+
```c#
76+
MyMvc.Controller<TestController>()
77+
MyMvc.ViewComponent<TestViewComponent>()
78+
MyMvc.Routing()
79+
```
80+
81+
It is up to you!
82+
83+
## Model state expression-based assertions
84+
85+
Instead of string-based model state assertions line in the **"ManageControllerTest"**:
86+
87+
```c#
88+
.ShouldHave()
89+
.ModelState(modelState => modelState
90+
.ContainingError(nameof(ChangePasswordViewModel.OldPassword))
91+
.ThatEquals("The Current password field is required.")
92+
.AndAlso()
93+
.ContainingError(nameof(ChangePasswordViewModel.NewPassword))
94+
.ThatEquals("The New password field is required.")
95+
.AndAlso()
96+
.ContainingNoError(nameof(ChangePasswordViewModel.ConfirmPassword)))
97+
```
98+
99+
You may use expression-based ones:
100+
101+
```c#
102+
.ShouldHave()
103+
.ModelState(modelState => modelState
104+
.For<ChangePasswordViewModel>()
105+
.ContainingErrorFor(m => m.OldPassword)
106+
.ThatEquals("The Current password field is required.")
107+
.AndAlso()
108+
.ContainingErrorFor(m => m.NewPassword)
109+
.ThatEquals("The New password field is required.")
110+
.AndAlso()
111+
.ContainingNoErrorFor(m => m.ConfirmPassword))
112+
```
113+
114+
## Expression-based route values
115+
116+
Instead of testing for redirects by using multiple method calls like in the **"ManageControllerTest"**:
117+
118+
```c#
119+
.ShouldReturn()
120+
.Redirect()
121+
.ToAction(nameof(ManageController.ManageLogins))
122+
.ContainingRouteValues(new { Message = ManageController.ManageMessageId.Error });
123+
```
124+
125+
You may use a single expression-based assertion call:
126+
127+
```c#
128+
.ShouldReturn()
129+
.Redirect()
130+
.To<ManageController>(c => c.ManageLogins(ManageController.ManageMessageId.Error))
131+
```
132+
133+
## Resolving route data
134+
135+
Let's test the **"AddressAndPayment"** action in the **"AccountController"**. We will validate for correct redirection:
136+
137+
```c#
138+
[Fact]
139+
public void AddressAndPaymentShouldRerurnRedirectWithValidData()
140+
=> MyController<CheckoutController>
141+
.Instance()
142+
.WithHttpRequest(request => request
143+
.WithFormField("PromoCode", "FREE"))
144+
.WithSession(session => session
145+
.WithEntry("Session", "TestCart"))
146+
.WithAuthenticatedUser()
147+
.WithDbContext(db => db
148+
.WithEntities(entities =>
149+
{
150+
var album = new Album { AlbumId = 1, Price = 10 };
151+
152+
var cartItem = new CartItem
153+
{
154+
Count = 1,
155+
CartId = "TestCart",
156+
AlbumId = 1,
157+
Album = album
158+
};
159+
160+
entities.Add(album);
161+
entities.Add(cartItem);
162+
}))
163+
.WithoutValidation()
164+
.Calling(c => c.AddressAndPayment(
165+
From.Services<MusicStoreContext>(),
166+
new Order { OrderId = 1 },
167+
With.No<CancellationToken>()))
168+
.ShouldReturn()
169+
.Redirect()
170+
.To<CheckoutController>(c => c.Complete(With.Any<MusicStoreContext>(), 1));
171+
```
172+
173+
Running this test will give us the following strange error message:
174+
175+
```
176+
When calling AddressAndPayment action in CheckoutController expected redirect result to have resolved location to '/Checkout/Complete/1', but in fact received '/Home/Complete/1'.
177+
```
178+
179+
The problem is that the request path is empty which results to the action route data being invalid. For that reason we are receiving wrong redirection location. The fix is easy - just call **"WithRouteData"**:
180+
181+
```c#
182+
[Fact]
183+
public void AddressAndPaymentShouldRerurnRedirectWithValidData()
184+
=> MyController<CheckoutController>
185+
.Instance()
186+
.WithHttpRequest(request => request
187+
.WithFormField("PromoCode", "FREE"))
188+
.WithSession(session => session
189+
.WithEntry("Session", "TestCart"))
190+
.WithAuthenticatedUser()
191+
.WithRouteData() // <---
192+
.WithDbContext(db => db
193+
.WithEntities(entities =>
194+
{
195+
var album = new Album { AlbumId = 1, Price = 10 };
196+
197+
var cartItem = new CartItem
198+
{
199+
Count = 1,
200+
CartId = "TestCart",
201+
AlbumId = 1,
202+
Album = album
203+
};
204+
205+
entities.Add(album);
206+
entities.Add(cartItem);
207+
}))
208+
.WithoutValidation()
209+
.Calling(c => c.AddressAndPayment(
210+
From.Services<MusicStoreContext>(),
211+
new Order { OrderId = 1 },
212+
With.No<CancellationToken>()))
213+
.ShouldReturn()
214+
.Redirect()
215+
.To<CheckoutController>(c => c.Complete(With.Any<MusicStoreContext>(), 1));
216+
```
217+
218+
The method call will resolve all the route values for you. The reason it is not done by default is because of performance considerations. You may manually provide route data values, if you need:
219+
220+
```c#
221+
.WithRouteData(new { controller = "Checkout" })
222+
```
223+
224+
The above issue may show when testing for the **"Created"** and **"Redirect"** action results. In some cases, the testing framework may catch the error and suggest you the following error:
225+
226+
```
227+
Route values are not present in the method call but are needed for successful pass of this test case. Consider calling 'WithRouteData' on the component builder to resolve them from the provided lambda expression or set the HTTP request path by using 'WithHttpRequest'.
228+
```
229+
230+
For example the test bellow with show the above message, if **"WithRouteData"** is not called because the **"ExternalLogin"** action uses **"IUrlHelper"**.
231+
232+
```c#
233+
[Fact]
234+
public void ExternalLoginShouldReturnCorrectResult()
235+
=> MyController<AccountController>
236+
.Instance()
237+
.WithRouteData()
238+
.Calling(c => c.ExternalLogin("TestProvider", "TestReturnUrl"))
239+
.ShouldReturn()
240+
.Challenge();
241+
```
242+
243+
## Additional attribute validations
244+
245+
Some packages expose additional attribute validations. For example adding the **"Microsoft.AspNetCore.Mvc.ViewFeatures"**, will add the option to test the **"AntiForgeryTokenAttribute"**. Instead of:
246+
247+
```c#
248+
.ContainingAttributeOfType<ValidateAntiForgeryTokenAttribute>()
249+
```
250+
251+
You can use:
252+
253+
```c#
254+
.ValidatingAntiForgeryToken()
255+
```
256+
257+
## Section summary
258+
259+
With this section we finished with the most important parts of the fluent assertion API. Few non-syntax related topics left and you are free to go. Go to the [Organizing Tests](/tutorial/organizingtests.html) section to see the various different ways you can write your tests!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Organizing Tests

docs/_docfx/tutorial/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@
3434
- name: Routing
3535
href: routing.md
3636
- name: Various Helpers
37-
href: helpers.md
37+
href: helpers.md
38+
- name: Organizing Tests
39+
href: organizingtests.md

docs/manifest.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)