W końcu napisałem swoje pierwsze testy. Może nie pierwsze testy w życiu, bo kiedyś na studiach miałem okazję używać asercji w testach jednostkowych, ale to nie były wyrafinowane przykłady, ot taka wzmianka. A dziś pełen profesjonalizm - testy integracyjne w natarciu, a może EndToEnd, tu nie mam pewności ;-)
Celem było napisanie testów sprawdzająych poprawność wywołania API dla UserController
. Po pierwsze utworzyłem nowy projekt o nazwie DataBoard.Tests.EndToEnd
za pomocą polecenia dotnet new xunit
. Tym samym jako bibliotekę wspomagającą testowanie wskazałem xUnit. Dodatkowo aby móc skorzystać z serwera testowego dodałem zależność Microsoft.AspNetCore.TestHost
. Finalnie należy dodać następujące biblioteki wymagane do testów ASP.NET Core.
<PackageReference Include="xunit" Version="2.2.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0"/>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.2"/>
Kolejnym krokiem jest uruchomienie serwera testowego, który wykorzystuje klasę Startup
z projektu DataBoard.Web
.
public class UsersControllerTest
{
private readonly TestServer _server;
private readonly HttpClient _client;
public UsersControllerTest()
{
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_client = _server.CreateClient();
}
}
Teraz pozostało tylko utworzenie testów. Każda testowa metoda musi zostać oznaczona atrybutem Fact
.
Pierwszy test sprawdza czy zwracany jest poprawny użytkownik na podstawie adresu e-mail. Porównuję zwrócony e-mail z żądanym, dodatkowo weryfikuję kod odpowiedzi.
[Fact]
public async Task given_valid_email_user_should_exist()
{
var email = "[email protected]";
var response = await _client.GetAsync($"/users/{email}");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
var user = JsonConvert.DeserializeObject<UserDto>(responseString);
Assert.Equal(email, user.Email);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Drugi test sprawdza, czy przy próbie żądania użytkownika na podstawie nie istniejącego adresu e-mail zostanie zwrócony status 404.
public async Task given_valid_email_user_should_not_exist()
{
var email = "[email protected]";
var response = await _client.GetAsync($"/users/{email}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
Trzeci test sprawdza wywołanie żądania POST - utworzenie nowego użytkownika. Ten test był najtrudniejszy, ponieważ do metody PostAsync
poza URI musiałem przekazać obiekt typu HttpContent, który jest klasą abstrakcyjną i powinien zawierać tak naprawdę treść żądania HTTP. W “zwykłym” ASP.NET istnieje metoda PostAsJsonAsync, niestety .NET Core jest ubogi o nią. Jako rozwiązanie problemu utworzyłem pomocniczą klasę JsonContent
, która dziedziczy po StringContent
, a ta po HttpContent
. W konstruktorze przekazuje obiekt do serializacji. Natomiast sama metoda testowa sprawdza status HTTP oraz zwrócony nagłówek żądania HTTP.
public class JsonContent : StringContent
{
public JsonContent(object obj) : base(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json")
{
}
}
[Fact]
public async Task given_unique_email_user_should_be_created()
{
var command = new CreateUser();
command.Email = "[email protected]";
command.Password = "secret";
var payload = new JsonContent(command);
var response = await _client.PostAsync("users/", payload);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal(response.Headers.Location.ToString(), $"/users/{command.Email}");
}
Samo uruchomienie testów jest proste, wystarczy wywołać polecenie dotnet test
.
Na pewno moje testy nie są idealne i wymagają poprawek, ale mają niepodważalną zaletę po prostu są ;-) Z rzeczy, które należy zmienić to na pewno wywołanie testów na repozytorium InMemory, a nie bezpośrednio na bazie danych, ponieważ ostatni test given_unique_email_user_should_be_created
wykona się tylko raz dla żądanego adresu e-mail. Muszę doczytać w jaki sposób dla projektu testowego zmienić repozytorium i zrefaktorować kod, ale to już treść na następny odcinek.