Skip to content

Commit 78808e0

Browse files
committed
Added Controllers section to the tutorial (#132)
1 parent dd11f11 commit 78808e0

13 files changed

Lines changed: 505 additions & 112 deletions
130 KB
Loading
45.8 KB
Loading
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Controllers
2+
3+
In this section we will dive a bit deeper into controller testing. By understanding it, you will get familiar with the fundamentals of My Tested ASP.NET Core MVC and see how a lot of other components from a typical MVC web application are asserted in a similar manner. Of course we will use the classical AAA (Arrange, Act, Assert) approach.
4+
5+
## Arrange
6+
7+
Go to the **"ManageController"** again and analyse the **"ChangePassword"** action. You will notice that with invalid model state this action returns view result with the same model provided as a request parameter. Particularly, we want to test these lines of code:
8+
9+
```c#
10+
if (!ModelState.IsValid)
11+
{
12+
return View(model);
13+
}
14+
```
15+
16+
My Tested ASP.NET Core MVC provides a very easy way to arrange the model state, but we will ignore it for now. What we want is to add a model error to the action call manually. Go to the **"ManageControllerTest"** class, add the new test and start with the typical selection of a controller to test:
17+
18+
```c#
19+
[Fact]
20+
public void ChangePassword_ShouldReturn_ViewWithSameModel_WithInvalidModelState()
21+
{
22+
MyController<ManageController>
23+
.Instance()
24+
}
25+
```
26+
27+
We will now examine three different ways to arrange the model state on the tested **""ManageController"**. Since the model state is part of the controller (action) context (see [HERE](https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs#L83)), you may instantiate one (after adding the **"Microsoft.AspNetCore.Mvc"** using) and provide it by using the **"WithControllerContext"** (**"WithActionContext"**) method:
28+
29+
```c#
30+
var controllerContext = new ControllerContext();
31+
controllerContext.ModelState.AddModelError("TestError", "TestErrorMessage");
32+
33+
MyController<ManageController>
34+
.Instance()
35+
.WithControllerContext(controllerContext)
36+
```
37+
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:
39+
40+
```c#
41+
MyController<ManageController>
42+
.Instance()
43+
.WithControllerContext(context => context.ModelState
44+
.AddModelError("TestError", "TestErrorMessage"))
45+
```
46+
47+
These are fine but the model state dictionary can be accessed directly from the controller itself, so we can just skip the whole **"ControllerContext"** class by using the **"WithSetup"** method:
48+
49+
```c#
50+
MyController<ManageController>
51+
.Instance()
52+
.WithSetup(controller => controller.ModelState
53+
.AddModelError("TestError", "TestErrorMessage"))
54+
```
55+
56+
As you can see the **"WithSetup"** method will come in handy wherever the fluent API does not provide a specific arrange method. As a side note - My Tested ASP.NET Core MVC provides an easy way to set up the model state dictionary, but we will cover it later in this tutorial.
57+
58+
Each one of these three ways for arranging the controller is fine but stick with the third option for now.
59+
60+
## Act
61+
62+
We need to act! In other words we need to call the action method. We do not need an actual request model to test the desired logic, so let's pass a null value as a parameter. Add this line to the test:
63+
64+
```c#
65+
.Calling(c => c.ChangePassword(null))
66+
```
67+
68+
You should be familiar with the **"Calling"** method from the previous sections. Again, if you prefer to be more expressive, you may use the **"With"** class:
69+
70+
```c#
71+
.Calling(c => c.ChangePassword(With.No<ChangePasswordViewModel>()))
72+
```
73+
74+
Well, this was easy! :)
75+
76+
## Assert
77+
78+
The final part of our test is asserting the action result. You should know how to assert a view result too, so add these to the test:
79+
80+
```c#
81+
.ShouldReturn()
82+
.View();
83+
```
84+
85+
We now need to test the returned model. It should be the same as the one provided through the action parameter. If you look through the intellisense after the **"View"** call, you will not find anything related to models. The reason simple - model testing is available in a separate package which we will install in the next section.
86+
87+
For now we will use the tools we already have imported in our test project. Introducing the magical **"ShouldPassForThe<TWhateverYouLike>"** method! I know developers do not like magic code but this one is cool, I promise! :)
88+
89+
Add the following lines to the test:
90+
91+
```c#
92+
.AndAlso()
93+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Null(viewResult.Model));
94+
```
95+
96+
Now run the test and we are ready! :)
97+
98+
But before moving on with our lives, let's explain these two lines.
99+
100+
First - the **"AndAlso"** method. It is just there for better readability and expressiveness. It is available on various places in the fluent API but it actually does nothing most of the time. You may remove it from your code now, then recompile it and run the test again and it will still pass. Of course, it is up to you whether or not to use the **"AndAlso"** method but admit it - it is a nice little addition to the test! :)
101+
102+
Second - the magical **"ShouldPassForThe<ViewResult>"** call. To make sure it works correctly, let's change the **"Assert.Null"** to **"Assert.NotNull"** and run the test. It should fail loud and clear with the original **"xUnit"** message:
103+
104+
```
105+
Assert.NotNull() Failure
106+
```
107+
108+
Return back the **"Null"** call so that the test passes again. The **"ShouldPassForThe<TComponent>"** method obviously works. What is interesting here is that the generic parameter **"TComponent"** can be anything you like, as long it is recognised by My Tested ASP.NET Core MVC. Seriously, add the following to the test and run the test:
109+
110+
```c#
111+
.ShouldReturn()
112+
.View()
113+
.AndAlso()
114+
.ShouldPassForThe<Controller>(controller =>
115+
{
116+
Assert.NotNull(controller);
117+
Assert.True(controller.ModelState.ContainsKey("TestError"));
118+
})
119+
.AndAlso()
120+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Null(viewResult.Model));
121+
```
122+
123+
Of course the first **"ShouldPassForThe"** call does not make any sense at all but it proves that everything related to the test can be asserted by using the method. You may even put a break point into the action delegate and debug it, if you like.
124+
125+
I guess that you already know it, but if you put an invalid and unrecognisable type for the generic parameter, for example **"ShouldPassForThe<XunitProjectAssembly>"**, you will receive an exception:
126+
127+
```
128+
XunitProjectAssembly could not be resolved for the 'ShouldPassForThe<TComponent>' method call.
129+
```
130+
131+
To continue, let's bring back the test to its last passing state:
132+
133+
```c#
134+
[Fact]
135+
public void ChangePassword_ShouldReturn_ViewWithSameModel_WithInvalidModelState()
136+
{
137+
MyController<ManageController>
138+
.Instance()
139+
.WithSetup(controller => controller.ModelState
140+
.AddModelError("TestError", "TestErrorMessage"))
141+
.Calling(c => c.ChangePassword(With.No<ChangePasswordViewModel>()))
142+
.ShouldReturn()
143+
.View()
144+
.AndAlso()
145+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Null(viewResult.Model));
146+
}
147+
```
148+
149+
We are still not asserting whether the view model is the same object as the provided method parameter. Let's change that by instantiating a **"ChangePasswordViewModel"** and test the action with it:
150+
151+
```c#
152+
[Fact]
153+
public void ChangePassword_ShouldReturn_ViewWithSameModel_WithInvalidModelState()
154+
{
155+
var model = new ChangePasswordViewModel
156+
{
157+
OldPassword = "OldPass",
158+
NewPassword = "NewPass",
159+
ConfirmPassword = "NewPass"
160+
};
161+
162+
MyController<ManageController>
163+
.Instance()
164+
.WithSetup(controller => controller.ModelState
165+
.AddModelError("TestError", "TestErrorMessage"))
166+
.Calling(c => c.ChangePassword(model))
167+
.ShouldReturn()
168+
.View()
169+
.AndAlso()
170+
.ShouldPassForThe<ViewResult>(viewResult => Assert.Same(model, viewResult.Model));
171+
}
172+
```
173+
174+
Our work here is done (for now)! :)
175+
176+
## Section summary
177+
178+
In this section we saw the AAA approach collaborating gracefully with My Tested ASP.NET Core MVC. However, I know you remember reading earlier about an easier way of arranging the model state and more fluent testing options for the view result models. You can learn about them in the [Models](/tutorial/models.html) section!

docs/_docfx/tutorial/debugging.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
11
# Debugging Failed Tests
22

3-
Now let's dive into [Controllers](/tutorial/controllers.html) testing!
3+
In this section we will learn how easy is to debug failing tests.
4+
5+
## Friendly error messages
6+
7+
Let's see how nice and friendly error message My Tested ASP.NET Core MVC provides on a failed test. Go to the **"ManageControllerTest"** and change the redirect action:
8+
9+
```c#
10+
.ToAction(nameof(ManageController.LinkLogin))
11+
```
12+
13+
Run the test and you will be provided with a detailed error message showing exactly what has failed:
14+
15+
```
16+
When calling RemoveLogin action in ManageController expected redirect result to have 'LinkLogin' action name, but instead received 'ManageLogins'.
17+
```
18+
19+
We can see in the above message that the redirect action is actually **"ManageLogins"** so let's return that value and try something else. Change the **"Message"** route value property to **"Error"**:
20+
21+
```c#
22+
.ToAction(nameof(ManageController.ManageLogins))
23+
.ContainingRouteValues(new { Error = ManageController.ManageMessageId.Error });
24+
```
25+
26+
Run the test again and you should see:
27+
28+
```
29+
When calling RemoveLogin action in ManageController expected redirect result route values to have entry with 'Error' key and the provided value, but such was not found.
30+
```
31+
32+
The library tells us that there is no **"Error"** key in the redirect route value dictionary. Now bring back the **"Message"** key to make the test pass again.
33+
34+
## Debugging the failing action
35+
36+
If the provided error messages are not enough to diagnose why the test fails, you can always use the good old C# debugger. Put a break point on the action method:
37+
38+
<img src="/images/tutorial/actiondebugging.jpg" alt="Debugging actions" />
39+
40+
Then click with the right mouse button on the failing test and select **"Debug Selected Tests"**:
41+
42+
<img src="/images/tutorial/debugselectedtests.jpg" alt="Debug through the test explorer" />
43+
44+
You know the drill from here on! :)
45+
46+
## Section summary
47+
48+
In this section we learned how helpful and developer-friendly is My Tested ASP.NET Core MVC with failed tests. But enough about failures and errors. Let's dive into the [Controllers](/tutorial/controllers.html) testing!

docs/_docfx/tutorial/gettingstarted.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Getting Started
22

3+
In this section we will learn how to configure My Tested ASP.NET Core MVC and get familiar with all the small issues we may encounter in the process.
4+
5+
## Prepare test assembly
6+
37
First things first - we need a test assembly! Open the [Music Store solution](https://raw.githubusercontent.com/ivaylokenov/MyTested.AspNetCore.Mvc/development/docs/_docfx/tutorial/MusicStore-Tutorial.zip), add **"test"** folder and create a new .NET Core class library called **"MusicStore.Test"** in it.
48

59
<img src="/images/tutorial/createtestproject.jpg" alt="Create .NET Core test assembly" />
@@ -41,7 +45,9 @@ Your **"project.json"** file should look like this:
4145

4246
You may need to change/update the versions of the listed packages with more recent ones.
4347

44-
Now let's write our first unit test. We will test the **"AddressAndPayment"** action in the **"CheckoutController"**. It is one of the most simplest actions possible - returns a default view no matter the HTTP request.
48+
## Our first test
49+
50+
Now let's write our first unit test. We will test the **"AddressAndPayment"** action in the **"CheckoutController"**. It is one of the simplest actions possible - returns a default view no matter the HTTP request.
4551

4652
<img src="/images/tutorial/addressandpaymentactions.jpg" alt="Simple controller action returning default view" />
4753

@@ -73,11 +79,13 @@ This should be your unit test now:
7379

7480
<img src="/images/tutorial/firstunittest.jpg" alt="First unit test returning simple view" />
7581

82+
## "TestStartup" class
83+
7684
Let's build the solution and run the test.
7785

7886
<img src="/images/tutorial/nostartuperror.jpg" alt="First unit test fails because of missing TestStartup class" />
7987

80-
Surpise! The simplest test fails. This testing framework is a waste of time! :(
88+
Surprise! The simplest test fails. This testing framework is a waste of time! :(
8189

8290
Joke! Don't leave yet! By default My Tested ASP.NET Core MVC requires a **"TestStartup"** file at the root of the test assembly so let's add one. Write the following code in it:
8391

@@ -96,6 +104,10 @@ namespace MusicStore.Test
96104
}
97105
```
98106

107+
You may have noticed the constructor of the **"CheckoutController"**. It is not an empty one. My Tested ASP.NET Core MVC uses the registered services from the **"TestStartup"** class to resolve all dependencies and instantiate the controller. We will get in more details about the test service provider later in this tutorial.
108+
109+
## Web configuration
110+
99111
Now run the test again.
100112

101113
<img src="/images/tutorial/configjsonerror.jpg" alt="First unit test fails because of missing config.json" />
@@ -140,6 +152,8 @@ Your **"config.json"** file should look like this:
140152

141153
Now run the test again in Visual Studio and... oh, miracle, it passes! :)
142154

155+
## Multiple frameworks
156+
143157
Don't be too happy yet as there is a (not-so) small problem here. Visual Studio runs the discovered tests only for the first targeted framework, which in our case is **"netcoreapp1.0"**. But how to test for the other one - **"net451"**?
144158

145159
Go to the **"MusicStore.Test"** project folder and open a console terminal there. The easiest way is pressing "SHIFT + Right Mouse Button" somewhere in the window and then clicking "Open command window here".
@@ -171,6 +185,8 @@ Go back to the console terminal and run **"dotnet test"** again.
171185

172186
Oh, miracles! The test passes correctly without any loud and ugly errors! Oh, yeah, do you feel the happiness? This library is really DA BOMB!!! :)
173187

188+
## Understanding the details
189+
174190
OK, back to that promise - the detailed explanation of all the fails. **Basically three things happened.**
175191

176192
**First**, My Tested ASP.NET Core MVC needs to resolve the services required by the different components in your web application - controllers, view components, etc. By default the test configuration needs a **"TestStartup"** class at the root of the test project from where it configures the global service provider. This is why we got an exception telling us we forgot to add it. Remember:
@@ -192,6 +208,8 @@ The JSON file is not optional and since we inherit from the original web **"Star
192208

193209
**Third**, as we noticed Visual Studio does not run the discovered tests for all specified frameworks. This is why we went to the console and tried running there. Unfortunately, the test failed for the **"net451"** framework. The reason is simple. The full framework did not save and store our project references and dependencies. This is why all the required test plugin classes could not be loaded when using "net451". By setting the **"preserveCompilationContext"** option to **"true"** the compiler will store the dependencies information into a file from where later during runtime it can be read successfully.
194210

211+
## Error messages
212+
195213
To finish this section let's make the test fail because of an invalid assertion just to see what happens. Instead of testing for **"View"**, make it assert for any other action result, for example **"BadRequest"**:
196214

197215
```c#
@@ -212,4 +230,6 @@ When calling AddressAndPayment action in CheckoutController expected result to b
212230

213231
Of course, you should undo the change and return the **"View"** call (unless you want a failing test during the whole tutorial but that's up to you again). :)
214232

233+
## Section summary
234+
215235
Well, all is well that ends well! While the **"Getting Started"** section of this tutorial may feel a bit "kaboom"-ish, it covers all the common failures and problems you may encounter while starting to use My Tested ASP.NET Core MVC. From now on it is all unicorns and rainbows. Go to the [Packages](/tutorial/packages.html) section and see for yourself! :)

docs/_docfx/tutorial/packages.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Packages
22

3-
In this section we will learn the most important parts of arranging and asserting our web application components. Of course, as a main building block of the ASP.NET Core MVC framework, we will start with controllers.
3+
In this section we will learn the most important parts of arranging and asserting our web application components.
44

5-
Before we begin, let's make a step backwards. Remember the **"project.json"** and the referenced **"MyTested.AspNetCore.Mvc"** dependency? Good! The My Tested ASP.NET Core MVC framework consists of many packages. Here are the most important ones:
5+
## The building blocks of the testing framework
6+
7+
Of course, as a main building block of the ASP.NET Core MVC framework, we will start with controllers. Before we begin, let's make a step backwards. Remember the **"project.json"** and the referenced **"MyTested.AspNetCore.Mvc"** dependency? Good! The My Tested ASP.NET Core MVC framework consists of many packages. Here are the most important ones:
68

79
- **"MyTested.AspNetCore.Mvc.Core"** - Contains setup and assertion methods for MVC core features - controllers, models and routes
810
- **"MyTested.AspNetCore.Mvc.DataAnnotations"** - Contains setup and assertion methods for data annotation validations and model state
@@ -20,6 +22,8 @@ Additionally, these two packages are also available:
2022

2123
Full list and descriptions of all available packages can be found [HERE](/guide/packages.html). All of them except the **"Lite"** one require a license code in order to be used without limitations. If a license code is not provided, a maximum of 100 assertions per test project is allowed. More information about the licensing can be found [HERE](/guide/licensing.html).
2224

25+
## Breaking down the MVC package
26+
2327
Now, let's get back to the testing. Go to the **"project.json"** file and replace the **"MyTested.AspNetCore.Mvc"** dependency with **"MyTested.AspNetCore.Mvc.Controllers"**. We will start using the small and specific packages for now and then we will switch to the **"Universe"** one later in the tutorial.
2428

2529
Your **"project.json"** dependencies should look like this:
@@ -55,6 +59,8 @@ You can see this by examining the intellisense of the test result:
5559

5660
<img src="/images/tutorial/coreintellisense.jpg" alt="Controllers package intellisense" />
5761

62+
## Asserting core controllers
63+
5864
We will now try the core action results before returning back to the view features. Comment the first test for now (so that the project will compile with the currently added dependencies) and add a new class named **"ManageControllerTest"** in the **"Controllers"** folder. We will test the asynchronous **"RemoveLogin"** action in the **"ManageController"**. If you examine it, you will notice that it returns **"RedirectToAction"**, if no user is authenticated.
5965

6066
Add the necessary usings and write the following test into the **"ManageControllerTest"** class:
@@ -93,6 +99,8 @@ public void RemoveLogin_ShouldReturn_RedirectToAction_WithNoUser()
9399

94100
Now we can sleep peacefully! :)
95101

102+
## Adding view action results
103+
96104
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:
97105

98106
```json
@@ -107,4 +115,6 @@ OK, back to that commented test. We cannot test views with our current dependenc
107115

108116
This package adds all action results from the [Controller](https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs) class from the [ViewFeatures](https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNetCore.Mvc.ViewFeatures) MVC package. Go back to the **"CheckoutControllerTest"** class and uncomment the view test. It should compile and pass successfully now.
109117

118+
## Section summary
119+
110120
In this section we learned how we can use only these parts from My Tested ASP.NET Core MVC that we actually need in our testing project. As you can see each small package dependency adds additional extension methods to the fluent API. We will add more and more packages in the next sections so that you can get familiar with them. Next - [Debugging Failed Tests](/tutorial/debugging.html)!
130 KB
Loading
45.8 KB
Loading

docs/manifest.json

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

0 commit comments

Comments
 (0)