|
1 |
| -# Organizing Tests |
| 1 | +# Organizing Tests |
| 2 | + |
| 3 | +This section will cover the various ways we can organize our tests in a class. |
| 4 | + |
| 5 | +## The fluent style |
| 6 | + |
| 7 | +This is the test style we used so far. For example let's take a look at our **"StoreControllerTest"** class: |
| 8 | + |
| 9 | +```c# |
| 10 | +namespace MusicStore.Test.Controllers |
| 11 | +{ |
| 12 | + using Microsoft.Extensions.Options; |
| 13 | + using Mocks; |
| 14 | + using Models; |
| 15 | + using Moq; |
| 16 | + using MusicStore.Controllers; |
| 17 | + using MyTested.AspNetCore.Mvc; |
| 18 | + using System.Collections.Generic; |
| 19 | + using Xunit; |
| 20 | + |
| 21 | + public class StoreControllerTest |
| 22 | + { |
| 23 | + [Fact] |
| 24 | + public void IndexShouldReturnViewWithGenres() |
| 25 | + => MyController<StoreController> |
| 26 | + .Instance() |
| 27 | + .WithDbContext(dbContext => dbContext |
| 28 | + .WithEntities(entities => entities.AddRange( |
| 29 | + new Genre { Name = "FirstGenre" }, |
| 30 | + new Genre { Name = "SecondGenre" }))) |
| 31 | + .Calling(c => c.Index()) |
| 32 | + .ShouldReturn() |
| 33 | + .View() |
| 34 | + .WithModelOfType<List<Genre>>() |
| 35 | + .Passing(model => model.Count == 2); |
| 36 | + |
| 37 | + [Fact] |
| 38 | + public void BrowseShouldReturnNotFoundWithInvalidGenre() |
| 39 | + => MyController<StoreController> |
| 40 | + .Instance(new StoreController( |
| 41 | + MockProvider.MusicStoreContext, |
| 42 | + Mock.Of<IOptions<AppSettings>>())) |
| 43 | + .Calling(c => c.Browse("Invalid")) |
| 44 | + .ShouldReturn() |
| 45 | + .NotFound(); |
| 46 | + |
| 47 | + [Fact] |
| 48 | + public void BrowseShouldReturnCorrectViewModelWithValidGenre() |
| 49 | + => MyController<StoreController> |
| 50 | + .Instance() |
| 51 | + .WithServices( |
| 52 | + MockProvider.MusicStoreContext, |
| 53 | + Mock.Of<IOptions<AppSettings>>()) |
| 54 | + .Calling(c => c.Browse("Rap")) |
| 55 | + .ShouldReturn() |
| 56 | + .View() |
| 57 | + .WithModelOfType<Genre>() |
| 58 | + .Passing(model => model.GenreId == 2); |
| 59 | + } |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +## The classic AAA style |
| 64 | + |
| 65 | +You can split the fluent API in the classic Arrange-Act-Assert style: |
| 66 | + |
| 67 | +```c# |
| 68 | +[Fact] |
| 69 | +public void IndexShouldReturnViewWithGenres() |
| 70 | +{ |
| 71 | + // Arrange |
| 72 | + var controller = MyController<StoreController> |
| 73 | + .Instance() |
| 74 | + .WithDbContext(dbContext => dbContext |
| 75 | + .WithEntities(entities => entities.AddRange( |
| 76 | + new Genre { Name = "FirstGenre" }, |
| 77 | + new Genre { Name = "SecondGenre" }))); |
| 78 | + |
| 79 | + // Act |
| 80 | + var execution = controller.Calling(c => c.Index()); |
| 81 | + |
| 82 | + // Assert |
| 83 | + execution |
| 84 | + .ShouldReturn() |
| 85 | + .View() |
| 86 | + .WithModelOfType<List<Genre>>() |
| 87 | + .Passing(model => model.Count == 2); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +## The fluent AAA style |
| 92 | + |
| 93 | +Or just mark the fluent API with comments: |
| 94 | + |
| 95 | +```c# |
| 96 | +[Fact] |
| 97 | +public void IndexShouldReturnViewWithGenres() |
| 98 | + => MyController<StoreController> |
| 99 | + // Arrange |
| 100 | + .Instance() |
| 101 | + .WithDbContext(dbContext => dbContext |
| 102 | + .WithEntities(entities => entities.AddRange( |
| 103 | + new Genre { Name = "FirstGenre" }, |
| 104 | + new Genre { Name = "SecondGenre" }))) |
| 105 | + // Act |
| 106 | + .Calling(c => c.Index()) |
| 107 | + // Assert |
| 108 | + .ShouldReturn() |
| 109 | + .View() |
| 110 | + .WithModelOfType<List<Genre>>() |
| 111 | + .Passing(model => model.Count == 2); |
| 112 | +``` |
| 113 | + |
| 114 | +## The inheriting style |
| 115 | + |
| 116 | +You may inherit the **"MyController"** class to skip writing it in every single test: |
| 117 | + |
| 118 | +```c# |
| 119 | +namespace MusicStore.Test.Controllers |
| 120 | +{ |
| 121 | + using Microsoft.Extensions.Options; |
| 122 | + using Mocks; |
| 123 | + using Models; |
| 124 | + using Moq; |
| 125 | + using MusicStore.Controllers; |
| 126 | + using MyTested.AspNetCore.Mvc; |
| 127 | + using System.Collections.Generic; |
| 128 | + using Xunit; |
| 129 | + |
| 130 | + public class StoreControllerTest : MyController<StoreController> // <--- |
| 131 | + { |
| 132 | + [Fact] |
| 133 | + public void IndexShouldReturnViewWithGenres() |
| 134 | + => this |
| 135 | + .WithDbContext(dbContext => dbContext |
| 136 | + .WithEntities(entities => entities.AddRange( |
| 137 | + new Genre { Name = "FirstGenre" }, |
| 138 | + new Genre { Name = "SecondGenre" }))) |
| 139 | + .Calling(c => c.Index()) |
| 140 | + .ShouldReturn() |
| 141 | + .View() |
| 142 | + .WithModelOfType<List<Genre>>() |
| 143 | + .Passing(model => model.Count == 2); |
| 144 | + |
| 145 | + [Fact] |
| 146 | + public void BrowseShouldReturnNotFoundWithInvalidGenre() |
| 147 | + => Instance(new StoreController( |
| 148 | + MockProvider.MusicStoreContext, |
| 149 | + Mock.Of<IOptions<AppSettings>>())) |
| 150 | + .Calling(c => c.Browse("Invalid")) |
| 151 | + .ShouldReturn() |
| 152 | + .NotFound(); |
| 153 | + |
| 154 | + [Fact] |
| 155 | + public void BrowseShouldReturnCorrectViewModelWithValidGenre() |
| 156 | + => this |
| 157 | + .WithServices( |
| 158 | + MockProvider.MusicStoreContext, |
| 159 | + Mock.Of<IOptions<AppSettings>>()) |
| 160 | + .Calling(c => c.Browse("Rap")) |
| 161 | + .ShouldReturn() |
| 162 | + .View() |
| 163 | + .WithModelOfType<Genre>() |
| 164 | + .Passing(model => model.GenreId == 2); |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +**NOTE:** To run the above tests asynchronously, the test runner should instantiate the **"StoreControllerTest"** class for every single test. This is the default behaviour of xUnit so you should not experience any issues, if you do not alter its collection parallelism. You can also avoid a race condition if you replace the **"this"** keyword with **"Instance"**: |
| 170 | + |
| 171 | +```c# |
| 172 | +namespace MusicStore.Test.Controllers |
| 173 | +{ |
| 174 | + using Microsoft.Extensions.Options; |
| 175 | + using Mocks; |
| 176 | + using Models; |
| 177 | + using Moq; |
| 178 | + using MusicStore.Controllers; |
| 179 | + using MyTested.AspNetCore.Mvc; |
| 180 | + using System.Collections.Generic; |
| 181 | + using Xunit; |
| 182 | + |
| 183 | + public class StoreControllerTest : MyController<StoreController> |
| 184 | + { |
| 185 | + [Fact] |
| 186 | + public void IndexShouldReturnViewWithGenres() |
| 187 | + => Instance() // <--- |
| 188 | + .WithDbContext(dbContext => dbContext |
| 189 | + .WithEntities(entities => entities.AddRange( |
| 190 | + new Genre { Name = "FirstGenre" }, |
| 191 | + new Genre { Name = "SecondGenre" }))) |
| 192 | + .Calling(c => c.Index()) |
| 193 | + .ShouldReturn() |
| 194 | + .View() |
| 195 | + .WithModelOfType<List<Genre>>() |
| 196 | + .Passing(model => model.Count == 2); |
| 197 | + |
| 198 | + [Fact] |
| 199 | + public void BrowseShouldReturnNotFoundWithInvalidGenre() |
| 200 | + => Instance(new StoreController( |
| 201 | + MockProvider.MusicStoreContext, |
| 202 | + Mock.Of<IOptions<AppSettings>>())) |
| 203 | + .Calling(c => c.Browse("Invalid")) |
| 204 | + .ShouldReturn() |
| 205 | + .NotFound(); |
| 206 | + |
| 207 | + [Fact] |
| 208 | + public void BrowseShouldReturnCorrectViewModelWithValidGenre() |
| 209 | + => Instance() // <--- |
| 210 | + .WithServices( |
| 211 | + MockProvider.MusicStoreContext, |
| 212 | + Mock.Of<IOptions<AppSettings>>()) |
| 213 | + .Calling(c => c.Browse("Rap")) |
| 214 | + .ShouldReturn() |
| 215 | + .View() |
| 216 | + .WithModelOfType<Genre>() |
| 217 | + .Passing(model => model.GenreId == 2); |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +## Section summary |
| 223 | + |
| 224 | +Of course, you can always combine two or more of the above style as long as your code is consistent. Now, let's take a look at the framework's [Test Configuration](/tutorial/testconfig.html)! |
0 commit comments