Welcome to the twelfth episode of my course “Becoming a software developer” in which we will write tests, both unit and integration (end-to-end) for our application.
All of the materials including videos and sample projects can be downloaded from here.
The source code repository is being hosted on GitHub.
Scope
- Unit tests
- Integration tests
Abstract
Unit tests
You can read moe about testing here (the episode VII of this course), so I will not go into the details of this practice here. Instead, I want to tell you what is needed in order to start writing tests for the Passenger app. At first, I decided to use xUnit instead of NUnit, mostly due to the fact that I had some issues related to running NUnit tests after the latest update of the .NET Core framework to the version 1.1.
For the starters, including the following dependencies within the Passenger.Tests.csproj file:
1 2 3 4 5 6 7 |
<ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="FluentAssertions" Version="4.19.0" /> <PackageReference Include="Moq" Version="4.7.8" /> </ItemGroup> |
And let’s write a very basic tests – create a new directory called Services and add a new class UserServiceTests containing the following code:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserServiceTests { [Fact] public async Task register_async_should_invoke_add_async_on_repository() { var userRepositoryMock = new Mock<IUserRepository>(); var mapperMock = new Mock<IMapper>(); var userService = new UserService(userRepositoryMock.Object, mapperMock.Object); await userService.RegisterAsync("user@email.com", "user", "secret"); userRepositoryMock.Verify(x => x.AddAsync(It.IsAny<User>()), Times.Once); } } |
Do not forget about adding the required “using” to the missing namespaces. Eventually, run the dotnet test command and that’s it, your first unit test shall successfully pass!
Integration tests
Unit tests are easy, so what about creating sophisticated integration tests that will execute the real HTTP calls on our API? It could be done twofold – the first way is to run the API using dotnet run and write tests using e.g. HttpClient in order to send requests and validate them by using particular assertions.
However, there’s’ also another way, much cooler than that. Thanks to the ASP.NET Core framework, you can run the whole API in the memory and perform the integration tests this way, which is really cool. You can find more details here, but this is how could it look like.
At first, include the following dependencies within the Passenger.Tests.EndToEnd.csproj file:
1 2 3 4 5 6 7 8 |
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.1" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.01" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="FluentAssertions" Version="4.19.0" /> </ItemGroup> |
And let’s write the actual tests – create a new directory named Controllers and add a new class UsersControllerTests. Having done that, implement the following tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public class UsersControllerTests { protected readonly TestServer Server; protected readonly HttpClient Client; protected ControllerTestsBase() { Server = new TestServer(new WebHostBuilder() .UseStartup<Startup>()); Client = Server.CreateClient(); } [Fact] public async Task given_valid_email_user_should_exist() { var email = "user1@email.com"; var user = await GetUserAsync(email); user.Email.ShouldBeEquivalentTo(email); } [Fact] public async Task given_invalid_email_user_should_not_exist() { var email = "user1000@email.com"; var response = await Client.GetAsync($"users/{email}"); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound); } [Fact] public async Task given_unique_email_user_should_be_created() { var command = new CreateUser { Email = "test@email.com", Username = "test", Password = "secret" }; var payload = GetPayload(command); var response = await Client.PostAsync("users", payload); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.Created); response.Headers.Location.ToString().ShouldBeEquivalentTo($"users/{command.Email}"); var user = await GetUserAsync(command.Email); user.Email.ShouldBeEquivalentTo(command.Email); } protected static StringContent GetPayload(object data) { var json = JsonConvert.SerializeObject(data); return new StringContent(json, Encoding.UTF8, "application/json"); } } |
As before, make sure you do not forget about adding the required “using” to the missing namespaces. Finally, run the dotnet test command and that’s all!
Next
In the next episode, we’ll talk implement the Command Handler pattern and make use of the external Autofac (which is a powerful IoC container) in order to achieve such goal.
Great article! I like and keep track you post! 🙂
Pingback: Dew Drop - April 17, 2017 (#2459) - Morning Dew
Rzeczywiście artykuł jest bardzo dobry i nawet chciałem spróbować testów integracyjnych tyle, że w pełnym .net i OWIN. Samo uruchomienie aplikacji pod testy nie jest specjalnie trudne, bo składa się do napisania kilka linijek. Problemem jest operowanie na entity framework w pamięci, z którym nie mogłem sobie poradzić. Może jest ktoś w stanie pomóc poprzez link do artykułu lub może sam zna rozwiązanie, bo póki co nie wiem gdzie szukać.
Pozdrawiam
EF Core posiada mechanizm testowania w pamięci, a dokładniej uruchomienia bazy całkowicie w pamięci co pozwala napisać testy (zarówno jednostkowe jak i integracyjne). https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
No tak EF Core posiada, tylko ja działam pod EF 6(Poprostu śledząc kurs uczę się też .net 4.6, z którego nie czuje się jeszcze pewnie) i tutaj pojawia się ściana(przynajmniej dla mnie) bo dostaje jedynie na czerwono 3 wyjątki 1 z entity i 2 z mscorlib i status code 500.
Zmieniałeś linijkę w pliku Startup.cs z powiązaniem do InMemoryUserRepository? Bo nie widziałem, żebyś go zmieniał z AddScoped na AddSingleton, ani nigdzie nie wspominałeś o tym, a wszystko działa tak jakbyś miał właśnie użyty AddSingleton.
Patrząc na poprzedni odcinek XI i obecny XII, użycie:
services.AddScoped();
prowadzi do tego, że za każdym zapytaniem będzie nowa instancja? InMemoryUserRepository, przez co np. w odcinku XI można było zauważyć, że przy z każdym zapytaniem o tego samego użytkownika jest inne Id, a w obecnym odcinku XII nie przechodzi test given_unique_email_user_should_be_created(), bo są 2 zapytania w jednym teście przez co po utworzeniu użytkownika, chcąc wysłać zapytanie GET o utworzonego użytkownika to on już nie istnieje, a widzę, że u Ciebie wszystko działa tak jak należy, stąd też moje pytanie 🙂
Rejestrujesz interfejs jako singleton lub ustawiasz statyczną kolekcję obiektów w jego implementacji (oczywiście aby to było w pełni bezpieczne odnośnie wielowątkowości należy wybrać typ dedykowany Concurrent).
Pingback: Becoming a software developer - episode XIII [PL] - Python In Business Archive