Skip to content

Commit 7645b1d

Browse files
committed
Added Database section to the tutorial (#132)
1 parent 2793ffc commit 7645b1d

14 files changed

Lines changed: 553 additions & 32 deletions

File tree

docs/_docfx/troubleshoot/toc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
- name: Type Initializer Exception
55
href: typeinitexception.md
66
- name: Visual Studio Random Results
7-
href: typeinitexception.md
7+
href: randomtestresults.md
-53 Bytes
Binary file not shown.

docs/_docfx/tutorial/controllers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Return back the **"Null"** call so that the test passes again. The **"ShouldPass
122122

123123
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.
124124

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:
125+
I guess that you already know it, but if you put an invalid and unrecognisable type for the generic parameter, for example **"XunitProjectAssembly"**, you will receive an exception:
126126

127127
```
128128
XunitProjectAssembly could not be resolved for the 'ShouldPassForThe<TComponent>' method call.

docs/_docfx/tutorial/database.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Database
2+
3+
In this section you will get familiar with how helpful the fluent testing library is with an Entity Framework Core database. Despite the data storage abstraction you use (repository pattern, unit of work, etc.), **"DbContext"** testing has never been easier. And you don't even need a mocking framework! How cool is that? :)
4+
5+
## The scoped in memory database
6+
7+
Let's try to test an action using the **"DbContext"**. An easy one is **"Index"** in **"StoreController"**. Create a **"StoreControllerTest"** class, add the necessary usings and try to test the action:
8+
9+
```c#
10+
[Fact]
11+
public void Index_ShouldReturn_ViewWithGenres()
12+
=> MyController<StoreController>
13+
.Instance()
14+
.Calling(c => c.Index())
15+
.ShouldReturn()
16+
.View()
17+
.WithModelOfType<List<Genre>>();
18+
```
19+
20+
A nice little test. With a big "KABOOM"!
21+
22+
```
23+
When calling Index action in StoreController expected no exception but AggregateException (containing ArgumentException with 'Format of the initialization string does not conform to specification starting at index 0.' message) was thrown without being caught.
24+
```
25+
26+
Not cool for sure! The exception occurs because our **"config.json"** file contains a dummy (and invalid) connection string:
27+
28+
```c#
29+
"Data": {
30+
"DefaultConnection": {
31+
"ConnectionString": "Test Connection"
32+
}
33+
}
34+
```
35+
36+
And we should be happy about it! The last thing we want is our tests knowing where the application database is.
37+
38+
But we still need to write a test against the **"DbContext"**! Fear no more - go to the **"project.json"** file and add ""*MyTested.AspNetCore.Mvc.EntityFrameworkCore*"" as a dependency:
39+
40+
```json
41+
"dependencies": {
42+
"dotnet-test-xunit": "2.2.0-*",
43+
"xunit": "2.2.0-*",
44+
"MyTested.AspNetCore.Mvc.Controllers": "1.0.0",
45+
"MyTested.AspNetCore.Mvc.EntityFrameworkCore": "1.0.0", // <---
46+
"MyTested.AspNetCore.Mvc.ModelState": "1.0.0",
47+
"MyTested.AspNetCore.Mvc.Models": "1.0.0",
48+
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0",
49+
"MusicStore": "*"
50+
},
51+
```
52+
53+
Now run the test again and see the magic! :)
54+
55+
Wuuut! I can't believe it! It passes! And we didn't even touch the code! There must be some voodoo involved around here!
56+
57+
As we mentioned earlier - no developer should love magic so here it is the trick revealed. The **"EntityFrameworkCore"** package contains a test plugin, which recognises the **"DbContext"** related services and replaces them with scoped in memory ones. More information about the test plugins can be found [HERE](/guide/plugins.html).
58+
59+
Our test passes but it will be better if we assert the action with actual data. Change the test to:
60+
61+
```c#
62+
MyController<StoreController>
63+
.Instance()
64+
.WithDbContext(dbContext => dbContext
65+
.WithEntities(entities => entities.AddRange(
66+
new Genre { Name = "FirstGenre" },
67+
new Genre { Name = "SecondGenre" })))
68+
.Calling(c => c.Index())
69+
.ShouldReturn()
70+
.View()
71+
.WithModelOfType<List<Genre>>()
72+
.Passing(model => model.Count == 2);
73+
```
74+
75+
The good part of this test is the fact that these data objects live only in memory and are not stored anywhere.
76+
77+
The best part of the test is the fact that these data objects live in scoped per test lifetime. We will dive deeper into scoped services in the next tutorial section. For now, write those two tests and run them:
78+
79+
```c#
80+
[Fact]
81+
public void Index_ShouldReturn_ViewWithGenres()
82+
=> MyController<StoreController>
83+
.Instance()
84+
.WithDbContext(dbContext => dbContext
85+
.WithEntities(entities => entities.AddRange(
86+
new Genre { Name = "FirstGenre" },
87+
new Genre { Name = "SecondGenre" })))
88+
.Calling(c => c.Index())
89+
.ShouldReturn()
90+
.View()
91+
.WithModelOfType<List<Genre>>()
92+
.Passing(model => model.Count == 2);
93+
94+
[Fact]
95+
public void I_Will_Show_Scoped_Database_Services()
96+
=> MyController<StoreController>
97+
.Instance()
98+
.WithDbContext(dbContext => dbContext
99+
.WithEntities(entities => entities.AddRange(
100+
new Genre { Name = "ThirdGenre" })))
101+
.Calling(c => c.Index())
102+
.ShouldReturn()
103+
.View()
104+
.WithModelOfType<List<Genre>>()
105+
.Passing(model => model.Count == 1 && model.All(g => g.Name == "ThirdGenre"));
106+
```
107+
108+
Both tests pass successfully. They are almost the same but you can notice the difference in the database objects. The first test adds two entities and passes the predicate expecting two objects in the returned list, the second test adds another entity and passes the expectation of having a single genre with a specific name. It is obvious the database is fresh, clean and empty while running each test. This is the power of scoped test services - they allow each test to be run in isolation and in asynchronous environment.
109+
110+
## Asserting saved database changes
111+
112+
Remove the second test as it is not needed. We will now examine how we can assert saved database objects. For this purpose we are going to use the **"Create"** action (the HTTP POST one) in the **"StoreManagerController"** (located in the **"Admin"** area). The action expects an **"IMemoryCache"** service and since we will cover caching later in this tutorial, we will need a cache mock. Add **"Moq"** to the **"project.json"** dependencies:
113+
114+
```json
115+
"dependencies": {
116+
"dotnet-test-xunit": "2.2.0-*",
117+
"xunit": "2.2.0-*",
118+
"Moq": "4.6.38-*", // <---
119+
"MyTested.AspNetCore.Mvc.Controllers": "1.0.0",
120+
"MyTested.AspNetCore.Mvc.EntityFrameworkCore": "1.0.0",
121+
"MyTested.AspNetCore.Mvc.ModelState": "1.0.0",
122+
"MyTested.AspNetCore.Mvc.Models": "1.0.0",
123+
"MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0",
124+
"MusicStore": "*"
125+
},
126+
```
127+
128+
Create a **"StoreManagerControllerTest"**, add the necessary usings and write the following test:
129+
130+
```c#
131+
[Fact]
132+
public void Create_ShouldSaveAlbum_WithValidModelState_And_Redirect()
133+
{
134+
var album = new Album
135+
{
136+
AlbumId = 1,
137+
Title = "TestAlbum",
138+
Price = 50
139+
};
140+
141+
MyController<StoreManagerController>
142+
.Instance()
143+
.Calling(c => c.Create(
144+
album,
145+
Mock.Of<IMemoryCache>(),
146+
With.Default<CancellationToken>()))
147+
.ShouldHave()
148+
.ValidModelState()
149+
.AndAlso()
150+
.ShouldHave()
151+
.DbContext(db => db
152+
.WithSet<Album>(albums => albums
153+
.Any(a => a.AlbumId == album.AlbumId)))
154+
.AndAlso()
155+
.ShouldReturn()
156+
.Redirect()
157+
.ToAction(nameof(StoreManagerController.Index));
158+
}
159+
```
160+
161+
The actual database assertion is in the following lines:
162+
163+
```c#
164+
.ShouldHave()
165+
.DbContext(db => db
166+
.WithSet<Album>(albums => albums
167+
.Any(a => a.AlbumId == album.AlbumId)))
168+
```
169+
170+
My Tested ASP.NET Core MVC validates that the database set of albums should have the saved album with the correct **"AlbumdId"**. As with the previous example, the in memory database will be empty before the test runs. You may notice the **"With.Default"** call. It is just a more expressive way to write **"new CancellationToken()"**. Providing **"CancellationToken.None"** is also an option.
171+
172+
## Repository pattern
173+
174+
We will take a look at the repository pattern as a small deviation from the Music Store web application. As long as you use the Entity Framework Core **"DbContext"** class in your web application, the scoped in memory database will work correctly no matter the data abstractions layer. Imagine we had the following repository registered as a service in our web application:
175+
176+
```c#
177+
public class Repository<T> : IRepository<T>
178+
where T : class
179+
{
180+
private readonly MyDbContext db;
181+
182+
public Repository(MyDbContext db)
183+
{
184+
this.db = db;
185+
}
186+
187+
public IQueryable<T> All() => this.db.Set<T>();
188+
}
189+
```
190+
191+
And a controller using it:
192+
193+
```c#
194+
public class HomeController : Controller
195+
{
196+
private IRepository<Album> albums;
197+
198+
public HomeController(IRepository<Album> albums)
199+
{
200+
this.albums = albums;
201+
}
202+
203+
public IActionResult Index()
204+
{
205+
var latestAlbums = this.albums
206+
.All()
207+
.OrderByDescending(a => a.AlbumId)
208+
.Take(10)
209+
.ToList();
210+
211+
return this.Ok(latestAlbums);
212+
}
213+
}
214+
```
215+
216+
Testing the **"Index"** action does not require anything more than adding lots of albums to the **"DbContext"** and test whether the result list contains exactly 10 elements (you may test the sorting too):
217+
218+
```c#
219+
MyController<HomeController>
220+
.Instance()
221+
.WithDbContext(db => db
222+
.WithSet<Album>(set => AddAlbums(set)))
223+
.Calling(c => c.Index())
224+
.ShouldReturn()
225+
.Ok()
226+
.WithModelOfType<List<Album>>()
227+
.Passing(model => model.Count == 10);
228+
```
229+
230+
Piece of cake! :)
231+
232+
## Section summary
233+
234+
This section showed you one of the many useful built-in services suitable for writing fast and asynchronous tests for the ASP.NET Core Framework. A lot of web applications use a database layer so it is a crucial point to have a nice and easy way to assert it without having to lose a lot of development time in writing mocks or stubs. Now, head over to the next important part of our journey - the test [Services](/tutorial/services.html)!

docs/_docfx/tutorial/gettingstarted.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Type **"dotnet test"** and hit **"Enter**".
166166

167167
Oh, well... This does not look good... Fail again! Why are you even doing this tutorial, you may wonder? :(
168168

169-
If you still decide to stick around - .NET Core runs our test fine but the full .NET framework fails because it cannot load correctly all the plugin dependencies My Tested ASP.NET Core MVC needs (more information available [HERE](/guide/plugins.html).
169+
If you still decide to stick around - .NET Core runs our test fine but the full .NET framework fails because it cannot load correctly all the plugin dependencies My Tested ASP.NET Core MVC needs (more information available [HERE](/guide/plugins.html)).
170170

171171
Do not worry, this one is easy. Go to the test assembly's **"project.json"** file and set the **"preserveCompilationContext"** option under **"buildOptions"** to **"true"**:
172172

@@ -232,4 +232,4 @@ Of course, you should undo the change and return the **"View"** call (unless you
232232

233233
## Section summary
234234

235-
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! :)
235+
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/intro.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ We will use:
88
- Visual Studio Community - the best ASP.NET Core IDE - available [HERE](https://www.visualstudio.com/vs/community/)
99
- .NET Core CLI - the command line tools for .NET Core - get them from [HERE](https://www.microsoft.com/net/core)
1010
- xUnit - asynchronous C# test runner - more information [HERE](http://xunit.github.io/)
11+
- Moq - mocking framework for .NET - more information [HERE](https://github.com/moq/moq4)
1112
- My Tested ASP.NET Core MVC - fluent assertion framework - more information [HERE](https://mytestedasp.net/Core/Mvc)
1213

1314
The Music Store project is perfectly suitable for the tutorial because it contains commonly used components of a typical ASP.NET Core MVC web application - Entity Framework Core database, authenticated users, session, caching, services and more.

docs/_docfx/tutorial/models.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ To test action result models, you need to add **"MyTested.AspNetCore.Mvc.Models"
113113

114114
```json
115115
"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-
},
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+
},
124124
```
125125

126126
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:
@@ -209,7 +209,7 @@ MyController<ManageController>
209209
.ShouldReturn()
210210
.View()
211211
.WithModelOfType<ChangePasswordViewModel>()
212-
.Passing(m => m == model);
212+
.Passing(viewModel => viewModel == model);
213213
```
214214

215215
Aaaand... our work here is done (this time for real)! :)

docs/_docfx/tutorial/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010
- name: Controllers
1111
href: controllers.md
1212
- name: Models
13-
href: models.md
13+
href: models.md
14+
- name: Database
15+
href: database.md

docs/manifest.json

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

0 commit comments

Comments
 (0)