Skip to content

Commit 2793ffc

Browse files
committed
Added Models section to the tutorial (#132)
1 parent 78808e0 commit 2793ffc

9 files changed

Lines changed: 483 additions & 8 deletions

File tree

docs/_docfx/tutorial/controllers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ MyController<ManageController>
3535
.WithControllerContext(controllerContext)
3636
```
3737

38-
Since the testing framework prepares for you everything it can before running the actual test case, you may skip the instantiation and use the other overload of the method using an action delegate:
38+
Since the testing framework prepares for you everything it can before running the actual test case by using the test service provider, you may skip the instantiation and use the other overload of the method using an action delegate:
3939

4040
```c#
4141
MyController<ManageController>

docs/_docfx/tutorial/models.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Models
2+
3+
In this section we will learn how to arrange the model state and assert the action result models.
4+
5+
## Model state validation
6+
7+
The **"ModelStateDictionary"** class is commonly used in a typical MVC application when the request method is **"POST"**. In the previous section we wrote this test specifying the model state error manually:
8+
9+
```c#
10+
var model = new ChangePasswordViewModel
11+
{
12+
OldPassword = "OldPass",
13+
NewPassword = "NewPass",
14+
ConfirmPassword = "NewPass"
15+
};
16+
17+
MyController<ManageController>
18+
.Instance()
19+
.WithSetup(controller => controller.ModelState
20+
.AddModelError("TestError", "TestErrorMessage"))
21+
.Calling(c => c.ChangePassword(model))
22+
.ShouldReturn()
23+
.View()
24+
.AndAlso()
25+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Same(model, viewResult.Model));
26+
```
27+
28+
To skip the manual arrange of the model state dictionary, we can use the built-in validation in My Tested ASP.NET Core MVC. It is quite easy to do - by default the testing framework will validate all models passed as action parameters. If you examine the **"ChangePasswordViewModel"**, you will notice the two required properties - **"OldPassword"** and **"NewPassword"**. So, if we provide our action method with null values for these two model properties, My Tested ASP.NET Core MVC will validate them by using the registered services in the **"TestStartup"** class we create earlier. So let's change the test and run it again:
29+
30+
```c#
31+
var model = new ChangePasswordViewModel();
32+
33+
MyController<ManageController>
34+
.Instance()
35+
.Calling(c => c.ChangePassword(model))
36+
.ShouldReturn()
37+
.View()
38+
.AndAlso()
39+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Same(model, viewResult.Model));
40+
```
41+
42+
The test still passes but it we examine the **"ChangePassword"** action, we will notice that the same result is returned from the action when the password fails to change. In other words - we are not sure which case is asserted with the above test. We can easily fix the issue by using the following line:
43+
44+
```c#
45+
.ShouldPassForThe<Controller>(controller => Assert.Equal(2, controller.ModelState.Count))
46+
```
47+
48+
However, there is always a better way! Go to the **"project.json"** file and add **"MyTested.AspNetCore.Mvc.ModelState"** as a dependency:
49+
50+
```json
51+
"dependencies": {
52+
"dotnet-test-xunit": "2.2.0-*",
53+
"xunit": "2.2.0-*",
54+
"MyTested.AspNetCore.Mvc.Controllers": "1.0.0",
55+
"MyTested.AspNetCore.Mvc.ModelState": "1.0.0", // <---
56+
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0",
57+
"MusicStore": "*"
58+
},
59+
```
60+
61+
Besides the **"ShouldReturn"**, there is another very helpful method for various kinds of assertions - **"ShouldHave"**. With **"ShouldHave"** you can test different kind of components after the action have been invoked. For example we want to check whether the model state has become invalid, so we need to add:
62+
63+
```c#
64+
.ShouldHave()
65+
.InvalidModelState()
66+
```
67+
68+
These lines will validate whether the model state is invalid after the action call. By providing an integer to the method, you can specify the total number of expected validation errors. More importantly, you can easily combine them with **"ShouldReturn"** by using **"AndAlso"**:
69+
70+
```c#
71+
var model = new ChangePasswordViewModel();
72+
73+
MyController<ManageController>
74+
.Instance()
75+
.Calling(c => c.ChangePassword(model))
76+
.ShouldHave()
77+
.InvalidModelState(withNumberOfErrors: 2)
78+
.AndAlso()
79+
.ShouldReturn()
80+
.View()
81+
.AndAlso()
82+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Same(model, viewResult.Model));
83+
```
84+
85+
Run the test to see it pass. If you change the **"InvalidModelState"** call to **"ValidModelState"**, you can see a nice descriptive error message:
86+
87+
```
88+
When calling ChangePassword action in ManageController expected to have valid model state with no errors, but it had some.
89+
```
90+
91+
If you want to be more specific, the fluent API allows testing for specific model state errors:
92+
93+
```c#
94+
.ShouldHave()
95+
.ModelState(modelState => modelState
96+
.ContainingError(nameof(ChangePasswordViewModel.OldPassword))
97+
.ThatEquals("The Current password field is required.")
98+
.AndAlso()
99+
.ContainingError(nameof(ChangePasswordViewModel.NewPassword))
100+
.ThatEquals("The New password field is required.")
101+
.AndAlso()
102+
.ContainingNoError(nameof(ChangePasswordViewModel.ConfirmPassword)))
103+
.AndAlso()
104+
```
105+
106+
There is a better way to test for specific model state errors, but more on that later (as always in this tutorial). :)
107+
108+
Most of the time you will want to run the validation during the action call. However, if for some reason you don't, add **"MyTested.AspNetCore.Mvc.DataAnnotations"** to your **"project.json" file and call ""*WithoutValidation*"" for the tested controller.
109+
110+
## Action result models
111+
112+
To test action result models, you need to add **"MyTested.AspNetCore.Mvc.Models"** as a dependency of the test assembly:
113+
114+
```json
115+
"dependencies": {
116+
"dotnet-test-xunit": "2.2.0-*",
117+
"xunit": "2.2.0-*",
118+
"MyTested.AspNetCore.Mvc.Controllers": "1.0.0",
119+
"MyTested.AspNetCore.Mvc.ModelState": "1.0.0",
120+
"MyTested.AspNetCore.Mvc.Models": "1.0.0", // <---
121+
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0",
122+
"MusicStore": "*"
123+
},
124+
```
125+
126+
By adding the above package, you will add another set of useful extension methods for all action results returning a model object. First, remove this line from the **"ChangePassword"** test:
127+
128+
```c#
129+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Same(model, viewResult.Model));
130+
```
131+
132+
Good! Now back to those extension methods - the first one is **"WithNoModel"**, which asserts for exactly what it says (as every other method in the library, of course) - whether the action result returns a null model. Add the method after the **"View"** call and run the test to see what happens:
133+
134+
```c#
135+
var model = new ChangePasswordViewModel();
136+
137+
MyController<ManageController>
138+
.Instance()
139+
.Calling(c => c.ChangePassword(model))
140+
.ShouldHave()
141+
.InvalidModelState()
142+
.AndAlso()
143+
.ShouldReturn()
144+
.View()
145+
.WithNoModel();
146+
```
147+
148+
We should receive error message with no doubt - our action return the same model after all:
149+
150+
```
151+
When calling ChangePassword action in ManageController expected to not have a view model but in fact such was found.
152+
```
153+
154+
Obviously this is not the method we need. :)
155+
156+
From here on we have two options - testing the whole model for deep equality or testing just parts of the model (the ones we care the most).
157+
158+
Let's see the deep equality:
159+
160+
```c#
161+
.View()
162+
.WithModel(model)
163+
```
164+
165+
Since we expect the action to return the same view model as the one provided as an action parameter, we just pass it to the **"WithModel"** method and it will be validated for us. Note that this test will also pass:
166+
167+
```c#
168+
var model = new ChangePasswordViewModel
169+
{
170+
ConfirmPassword = "TestValue"
171+
};
172+
173+
MyController<ManageController>
174+
.Instance()
175+
.Calling(c => c.ChangePassword(model))
176+
.ShouldHave()
177+
.InvalidModelState()
178+
.AndAlso()
179+
.ShouldReturn()
180+
.View()
181+
.WithModel(new ChangePasswordViewModel
182+
{
183+
ConfirmPassword = "TestValue"
184+
});
185+
```
186+
187+
Although the models are not pointing to the the same instance, My Tested ASP.NET Core MVC will validate them by comparing their properties deeply. It works perfectly with interfaces, collections, generics, comparables, nested models and [many more object types](https://github.com/ivaylokenov/MyTested.AspNetCore.Mvc/blob/development/test/MyTested.AspNetCore.Mvc.Abstractions.Test/UtilitiesTests/ReflectionTests.cs#L426).
188+
189+
Although it is cool and easy to use the deep equality, most of the time it is not worth it. Models which have a lot of data may need a lot of code to make the test pass successfully. Supporting such huge objects is also a tedious task.
190+
191+
Introducing the other model assertion options - **"WithModelOfType"** and **"Passing"**. These two methods combined can give you enough flexibility to test only what you need from the model object. **"WithModelOfType"** allows you to test only for the type of the action result model so let's use instead of **"WithModel"**:
192+
193+
```c#
194+
.View()
195+
.WithModelOfType<ChangePasswordViewModel>()
196+
```
197+
198+
The test will pass if you run it but you still need to assert whether the returned model was the same as the parameter one. Luckily, the **"Passing"** method takes a delegate which tests the action result model, allowing you to be as specific in your assertions as you see fit:
199+
200+
```c#
201+
var model = new ChangePasswordViewModel();
202+
203+
MyController<ManageController>
204+
.Instance()
205+
.Calling(c => c.ChangePassword(model))
206+
.ShouldHave()
207+
.InvalidModelState()
208+
.AndAlso()
209+
.ShouldReturn()
210+
.View()
211+
.WithModelOfType<ChangePasswordViewModel>()
212+
.Passing(m => m == model);
213+
```
214+
215+
Aaaand... our work here is done (this time for real)! :)
216+
217+
## Section summary
218+
219+
This section covered an important part of the testing framework. Almost all actions in ASP.NET Core MVC use some kind of request or response models. You will see more examples for model assertions in the tutorial but for now let's move to one of biggest components of the typical web application - the [Database](/tutorial/database.html)!

docs/_docfx/tutorial/packages.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ Now we can sleep peacefully! :)
101101

102102
## Adding view action results
103103

104-
OK, back to that commented test. We cannot test views with our current dependencies. Go to the **"project.json"** and add **"MyTested.AspNetCore.Mvc.ViewActionResults"** ас а dependency:
104+
OK, back to that commented test. We cannot test views with our current dependencies. Go to the **"project.json"** and add **"MyTested.AspNetCore.Mvc.ViewActionResults"** as a dependency:
105105

106106
```json
107107
"dependencies": {
108108
"dotnet-test-xunit": "2.2.0-*",
109109
"xunit": "2.2.0-*",
110110
"MyTested.AspNetCore.Mvc.Controllers": "1.0.0",
111-
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0",
111+
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0", // <---
112112
"MusicStore": "*"
113113
},
114114
```

docs/_docfx/tutorial/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
- name: Debugging Failed Tests
99
href: debugging.md
1010
- name: Controllers
11-
href: controllers.md
11+
href: controllers.md
12+
- name: Models
13+
href: models.md

docs/manifest.json

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

docs/tutorial/controllers.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ <h2 id="arrange" sourcefile="tutorial/controllers.md" sourcestartlinenumber="5"
8686
MyController&lt;ManageController&gt;
8787
.Instance()
8888
.WithControllerContext(controllerContext)
89-
</code></pre><p sourcefile="tutorial/controllers.md" sourcestartlinenumber="38" sourceendlinenumber="38">Since the testing framework prepares for you everything it can before running the actual test case, you may skip the instantiation and use the other overload of the method using an action delegate:</p>
89+
</code></pre><p sourcefile="tutorial/controllers.md" sourcestartlinenumber="38" sourceendlinenumber="38">Since the testing framework prepares for you everything it can before running the actual test case by using the test service provider, you may skip the instantiation and use the other overload of the method using an action delegate:</p>
9090
<pre sourcefile="tutorial/controllers.md" sourcestartlinenumber="40" sourceendlinenumber="45"><code class="lang-c#">MyController&lt;ManageController&gt;
9191
.Instance()
9292
.WithControllerContext(context =&gt; context.ModelState

0 commit comments

Comments
 (0)