Slik vil jeg heretter teste APIer

Etter 13 år som konsulent hos forrige kunde, startet jeg i august 2021 på nytt oppdrag med et “greenfield” prosjekt. Domenet var kjent og systemet kunne gjøres fra bunnen av.

Oppdraget innebar å lage et sett med nye APIer og en front end.

Verktøykassen i den nye oppdraget: 

.Net Core, C#,  Azure, Azure Devops, CosmosDb, SQL Server, AKS

Og fra første dag, hører jeg ekko fra NDC konferanser med Uncle Bob i bakgrunnen:

“Hei Keb, nå må alt testes, og du må gjøre jobben din skikkelig!” 

Så det tenkte jeg å prøve…

Fant denne artikkelen skrevet av Tim Deschryver:

Ble veldig inspirert og bestemte meg for å prøve ut følgende API teststrategi:

  • Jeg vil tilstrebe at alle tester gjøres utenfra og inn ved å gjøre HTTP requests som en vanlig API klient. Dersom endringer i implementasjonen opprettholder kontrakten og datagrunnlaget, skal API oppføre seg som før og dermed skal testene være stabile. 
  • Jeg vil gjerne unngå tjeneste-mocking, om mulig. Om testen bruker databaser, køer eller andre tilstandsbaserte tjenester i integrasjonstesten, så vil aktuell tilstand kunne observeres etter testen. Og testen vil vise at man faktisk har testet mot (sky-) tjenestene. 
  • Jeg vil bruke fluent assertions!
  • Tester som kjøres lokalt, skal også kjøres i build pipeline(Azure Devops). Ingen deploy gjøres uten 100% grønne integrasjons tester.
  • Integrasjonstester skal dekke over 80%.

Jeg bruker dot net 6.0 og visual studio community edition.

Har basert integrasjonstesting på eksempel fra docs.microsoft.com:

Tutorial: Create a web API with ASP.NET Core

Mitt testprosjekt er basert på delprosjektet:

https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-web-api/samples/6.0/TodoApiDTO.

Microsofts tutorial benytter http-repl for å teste APIene (og det kan godt være at det er en vel så god måte å teste på).

Jeg vil bruke dette prosjektet som utgangspunkt og tenkte å supplere eksemplet med med eget testprosjekt som inneholder:

  • XUNIT som test driver
  • Microsoft.AspNetCore.Mvc.Testing for å kunne utføre http-baserte utenfra og inn tester.
  • FluentAssertions kunne skrive assertions som f.eks “response.StatusCode.Should.Be(HttpStatusCode.Ok)”.
  • Buke EF Core migrate for å opprette databasen på nytt før alle tester starter.
  • Oppsett EF Core med SQL server lokalt og i Azure Devops.
  • .Net Core 6.0 (uten Startup-klassen).

Jeg har laget et startprosjekt som kan lastest ned.

Starter med å lage et xUnit testprosjekt.

Legg inn følgende nuget pakker i testprosjektet:

  • Microsoft.AspNetCore.Mvc.Testing (http tester utenfra og inn)
  • FluentAssertions (lettleselig assertions)
  • Microsoft.AspNetCore.JsonPatch (for å teste HTTP PATCH)

ToDoItemsController har følgende metode som skal testes:

 // POST: api/TodoItems
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
        {
            var todoItem = new TodoItem
            {
                IsComplete = todoItemDTO.IsComplete,
                Name = todoItemDTO.Name
            };
            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();
            return CreatedAtAction(
                nameof(GetTodoItem),
                new { id = todoItem.Id },
                ItemToDTO(todoItem));
        }

For å få ToDoTests til å teste controlleren med HTTP kall, må den få en httpklient som kan kalle controlleren. Her kommer WebApplicationFactory fra nuget pakken Microsoft.AspNetCore.Mvc.Testing inn i bildet. Lager også en “integrationtest.json” med settings som kun gjelder for integrasjonstesten.

Lager en ny klasse IntegrationTestWebApplicationFactory i testprosjektet som arver fra WebApplicationFactory.

Det er denne klassen som konfigurerer integrasjonstestmiljøet.

Legg merke til at WebApplicationFactory bruker <Program> (i 6.0) ikke <Startup> (pre 6.0). https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0

Og i 6.0 må man legge til i bunn av Program.cs:

public partial class Program { }

IntegrationTestWebApplicationFactory:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using TodoApi.Models;
using Xunit;
namespace TodoApi.Integrationtests
{
    public class IntegrationTestWebApplicationFactory:WebApplicationFactory<Program>
    {
        public IConfiguration Configuration { get; set; }
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                Configuration = BuildConfiguration();
                config.AddConfiguration(Configuration);
            });
        }
        
        private static IConfiguration BuildConfiguration()
        {
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("integrationtest.json")
                .AddEnvironmentVariables()
                .Build();
            return config;
        }
    }
}

I ToDoApi er det lagt opp til at databasen er “InMemory”.

Jeg ønsker å se dataene etter at testene har kjørt og gjenbruke de genererte dataene fra API tester til eksperimentell testing og for å bruke UI lokalt etter at alle testene er kjørt.

I integrationtest.json er det derfor lagt til en connection til localdb (fungerer lokalt og i Azure Devops pipeline, mer om det litt senere)

Legger tili test prosjektet integrationtest.json som konfigurerer default database connection.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TodoListIntegrationTest;Integrated Security=True;"
  }
}

Testklassen får tilgang til Web API i constructoren.

using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.JsonPatch;
using FluentAssertions;
using Newtonsoft.Json;
using TodoApi.Models;
using Xunit;
namespace TodoApi.Integrationtests
{
    public class ToDoTests: IClassFixture<IntegrationTestWebApplicationFactory>
    {
        private readonly HttpClient _client;
        public ToDoTests(IntegrationTestWebApplicationFactory fixture)
        {
            _client = fixture.CreateClient();
        }
    }
}

Første test er mot API et HTTP POST CreateTodoItem.

Når testklassen har http klienten konfigurert mot Web Api, settes HTTP testen opp.

Legg merke til fluent assertions.

using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.JsonPatch;
using FluentAssertions;
using Newtonsoft.Json;
using TodoApi.Models;
using Xunit;
namespace TodoApi.Integrationtests
{
    public class ToDoTests: IClassFixture<IntegrationTestWebApplicationFactory>
    {
        private readonly HttpClient _client;
        public ToDoTests(IntegrationTestWebApplicationFactory fixture)
        {
            _client = fixture.CreateClient();
        }
        [Fact]
        public async Task TestThat_TodoItems_CanBeCreated()
        {
            //POST
            var message = new TodoItemDTO{IsComplete = false, Name = "DotheFirstTest"};
            var item = await _client.PostAsJsonAsync<TodoItemDTO>("api/ToDoItems", message);
            item.EnsureSuccessStatusCode();
            var actual = await item.Content.ReadFromJsonAsync<TodoItemDTO>();
            actual.IsComplete.Should().BeFalse();
            actual.Name.Should().Be(message.Name);
            //GET
            var readItem = await _client.GetFromJsonAsync<TodoItem>($"api/TodoItems/{actual.Id}");
            readItem?.Name.Should().Be(message.Name);
        }
    }
}

Før testen kjøres, er Ef Core i aksjon for å lage databasen på nytt.

Utvider IntegrationTestWebApplicationFactory med interface IAsyncLifetime og implementerer InitializeAsync metoden for opprette databasen på nytt:

await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.MigrateAsync();

Nb! Vær sikker på at du gjør dette mot settings i “integrationtest.json” og at “integrationtest.json” peker på en localdb. Har jo prøvet og feilet.

Vet at det kan gå galt.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using TodoApi.Models;
using Xunit;
namespace TodoApi.Integrationtests
{
    public class IntegrationTestWebApplicationFactory:WebApplicationFactory<Program>,IAsyncLifetime
    {
        public IConfiguration Configuration { get; set; }
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                Configuration = BuildConfiguration();
                config.AddConfiguration(Configuration);
            });
        }
        public async Task InitializeAsync()
        {
            var config = BuildConfiguration();
            var connection = config.GetConnectionString("Default");
            var options = new DbContextOptionsBuilder<TodoContext>()
                .UseSqlServer(connection)
                .Options;
            var dbContext = new TodoContext(options);
            await dbContext.Database.EnsureDeletedAsync();
            await dbContext.Database.MigrateAsync();
            
           
        }
        private static IConfiguration BuildConfiguration()
        {
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("integrationtest.json")
                .AddEnvironmentVariables()
                .Build();
            return config;
        }
        public async Task DisposeAsync()
        {
          
        }
    }
}

Kjør testen med debug.

Det er utført en http POST mot API kontrolleren:

Testen er grønn:

Etter testen er det data i integrasjonstest databasen (definert i intregrationtest.json):

Om du får feilmelding som ligner på:

Har du kanskje ikke lagt inn public partial class Program { } i Program.cs (noe MS må fikse i senere versjoner?) https://stackoverflow.com/questions/69991983/deps-file-missing-for-dotnet-6-integration-tests

Dette oppsettet fungerer veldig bra hittil for mine formål, fint å kombinere med eksperimentelle UI tester etter et testrun. Det er lett å refaktorere implementasjonen og testene er fortsatt grønne.

Vil gjerne høre deres erfaringer, synspunkter ang API testing.

Her er det SQL server som var databasen, har gjort det samme med CosmosDB og vil skrive mer om Azure Devops pipeline og CosmosDB senere.

Kode fra dette innlegget på github:

https://github.com/programmereAs/TodoApiDTOEnd.git

Leave a Reply

Discover more from programmere as

Subscribe now to keep reading and get access to the full archive.

Continue reading