|
| 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! |
0 commit comments