Mówisz, że Twój kod działa, ale czy na pewno jesteś tego pewny? Wprowadzasz zmianę w kodzie i może czujesz, że twój projekt to tykająca bomba na produkcji. Testy jednostkowe, integracyjne, systemowe, akceptacyjne itp. pozwolą Ci spokojniej spać.  Jeśli natomiast temat testów jednostkowych nie jest za dobrze Ci znany polecam zajrzeć do wcześniejszego mojego wpisu o dobrych praktykach w implementacji testów jednostkowych. Teraz przejdźmy na kolejny poziom wtajemniczenia po testach jednostkowych, czyli testy integracyjne. Testy integracyjne wykonywane są w celu wykrycia defektów w interfejsach i interakcjach pomiędzy modułami.

Microsoft.AspNetCore.TestHost

W wpisie o Fluent Validation z ASP.NET Core Web API zweryfikowałem funkcjonalność API wykorzystując narzędzie Postman. Dzisiaj zautomatyzujemy powyższy proces wykorzystując TestServer i HttpClient. Na początek stwórzmy osobny projekt dla testów integracyjnych, można zastosować jedną z poniższych konwencji nazewnictwa:

  • ApiProjectName.Tests.EndToEnd;
  • ApiProjectName.Tests.Integration;
  • ApiProjectName.IntegrationTests.

Do projektu instalujemy paczkę Microsoft.AspNetCore.TestHost z NuGet. Po dołączeniu zależności możemy tworzyć i konfigurować serwer testowy na potrzeby testów. W celu uniknięcia powielania kodu wydzielę wspólną logikę powtarzającą się dla klas testujących kontrolery do klasy abstrakcyjnej TestBase.

public abstract class TestBase
{
    protected readonly TestServer _server;
    protected readonly HttpClient _client;

    protected TestBase()
    {
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>());
        _client = _server.CreateClient();
    }
}

W klasie abstrakcyjnej następuje konfiguracja i inicjalizacja serwera testowego i klienta. Aktualnie mamy możliwość obsługi żądań testowych na testowym hoście, bez konieczności korzystania z prawdziwego hosta.

Testy kontrolera

Czas na napisanie kilku przykładowych testów w xUnit sprawdzających poprawność wywołania API dla kontrolera z wpisu  Fluent Validation z ASP.NET Core Web API.  Do wcześniejszej wersji kontrolera dorzuciłem obsługę asynchroniczności, oraz wstrzykiwanie implementacji IMapper przez konstruktor, poniżej prezentuję aktualną wersję kodu PicturesController.
[Route("api/[controller]")]
public class PicturesController : Controller
{
    private readonly IPictureService _pictureService;
    private readonly IMapper _mapper;

    public PicturesController(IPictureService pictureService, IMapper mapper)
    {
        _pictureService = pictureService;
        _mapper = mapper;
    }

    [HttpGet("{pictureId}", Name = "GetPictureAsync")]
    public async Task<IActionResult> GetAsync(int pictureId)
    {
        var picture = await _pictureService.GetPictureAsync(pictureId);
        if (picture == null)
            return NotFound();
        var pictureResult = _mapper.Map<PictureDto>(picture);
        return Ok(pictureResult);
    }

    [HttpPost]
    public async Task<IActionResult> PostAsync([FromBody]PictureForCreation pictureForCreation)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var picture = _mapper.Map<Picture>(pictureForCreation);
        await _pictureService.AddPictureAsync(picture);
        var createdPicture = _mapper.Map<PictureDto>(picture);

        return CreatedAtRoute("GetPictureAsync", new { pictureId = createdPicture.Id }, createdPicture);
    }
}

W testach HttpClient używany jest do tworzenia żądań do testowego serwera i otrzymania odpowiedzi. Przypadek testowy dla żądania GET jest dość prosty, w przypadku żądań POST trzeba przygotować więcej danych. Należy stworzyć obiekt StringContent zawierający model, który dziedziczy po HttpContent.

public class PicturesControllerTest : TestBase
{
    [Fact]
    public async Task GetAsync_PictureDoesNotExist_NotFound()
    {
        var response = await _client.GetAsync("/api/pictures/186");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    [Fact]
    public async Task PostAsync_DescriptionIsEqualTitle_Invalid()
    {
        var newPicture = new PictureForCreation
        {
            Title = "Irises",
            Artist = "Vincent van Gogh",
            Description = "Irises",
            Height = 71.0f,
            Width = 93.0f
        };
        var content = JsonConvert.SerializeObject(newPicture);
        var stringContent = new StringContent(content, Encoding.UTF8, "application/json");

        var response = await _client.PostAsync("/api/pictures/", stringContent);

        var responseString = await response.Content.ReadAsStringAsync();
        Assert.Contains("The provided description should be different from the title", responseString);
    }

    [Fact]
    public async Task PostAsync_PictureIsValid_PictureShouldBeCreated()
    {
        var newPicture = new PictureForCreation
        {
            Title = "Irises",
            Artist = "Vincent van Gogh",
            Description = "The first owner was Julien Tanguy, a paint grinder and art dealer whose portrait van Gogh painted three times",
            Height = 71.0f,
            Width = 93.0f
        };
        var content = JsonConvert.SerializeObject(newPicture);
        var stringContent = new StringContent(content, Encoding.UTF8, "application/json");

        var response = await _client.PostAsync("/api/pictures/", stringContent);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}

Udało się TestServer działa i przykładowe testy integracyjne przechodzą.

successStatus

Podsumowanie

W wpisie przedstawiłem jak w prosty sposób można wykonać testy integracyjne kontrolera z wykorzystaniem serwera testowego dla aplikacji ASP.NET Core Web API. Ze względu na zależności do usług testy integracyjne będą wolniejsze od testów jednostkowych,  które są odseparowane. W powyższym przykładzie nie korzystałem bezpośrednio z bazy danych, tylko wykorzystałem po stronie repozytorium zwykłą kolekcje do przechowywania danych. Implementacje testów z bazą danych poruszę innym razem, ten temat czeka na razie na Trello w kolejce do realizacji. Celem testów integracyjnych nie jest zastąpienie testów jednostkowych, stanowią kolejną opcję w procesie testowania, którą warto znać i stosować.