Keep an eye on your app’s API

Tags: UWP, WebApi, .NET, Open-Source

There are a few things you don’t like to see happening when you’re a mobile developer using an API:

  • API goes offline (obvious)
  • API changes contract (both URI endpoint and data structure)
  • API returns incorrect data

If you’re relying on a 3rd-party API, not much you can do except for running integration tests against the endpoints. If your team is owning the API, then there are several options and the one I’d like to highlight today is MyTested.WebApi. It has great documentation, but as I’ve been using it at the client for several months now, I thought let’s give it a well-deserved extra piece of attention.

We currently use it to test if the contract doesn’t change (which can happen without you knowing in a larger team) and to verify that the API keeps returning valid data on all endpoints. Either of both can severely cripple your mobile application (or web site).

Route testing

The first point where your API breaks is the actual route. We’re using NUnit, but you can use the other test frameworks as well. Some key points we like to test here are:

  • The actual routing: does a given endpoint URI resolve to the correct method. This might sound stupid, but it solidifies your contract. If this test breaks, you know you’ll have to update your frontend (app or web site).
  • The parsing of the parameters on the URI or in the body. It wouldn’t be the first time that the format is incorrect.
  • Checking if you’re using the correct HTTP verb.
  • Explicitly checking that a certain action is not allowed or no longer supported.

Below are a few supported cases showing HTTP verbs, passing parameters, handling exceptions, … There are plenty more available.

[TestFixture]
public class SystemControllerRouteTests : ControllerRouteTestBase
{
    [Test]
    public void System_Get_Version_ShouldMapCorrectly()
    {
        // simple get            
        MyWebApi.Routes()
            .ShouldMap(RouteConstants.System.Version)
            .To<SystemController>(c => c.Version());
    }
   
    [Test]
    public void System_Get_TestException_ShouldMapCorrectly()
    {
        // get with uri parameter
        MyWebApi.Routes()
            .ShouldMap(string.Format(RouteConstants.System.TestException, "test"))
            .To<SystemController>(c => c.TestExceptionFilters("test"));
    }

    [Test]
    public void System_Put_TestPutFilters_ShouldMapCorrectly()
    {
        // put with some json
        MyWebApi.Routes()
            .ShouldMap(string.Format(RouteConstants.System.TestPutFilters, "1"))
            .WithHttpMethod(HttpMethod.Put)
            .WithJsonContent("{\"Id\":\"1\", \"Naam\":\"Test\"}")
            .To<SystemController>(c => c.TestPutFilters(new IdNaamObject() {Id = 1, Naam = "Test"}));
    }

    [Test]
    public void System_Delete_ClearCaches_ShouldMapCorrectly()
    {
        // delete
        MyWebApi.Routes()
            .ShouldMap(RouteConstants.System.ClearCaches)
            .WithHttpMethod(HttpMethod.Delete)
            .To<SystemController>(c => c.ClearCaches());
    }

    [Test]
    public void BugReport_Get_ThrowsException()
    {
        // should be post, not a get
        var exception = Assert.Throws<RouteAssertionException>(() =>
        {
            MyWebApi.Routes()
                .ShouldMap(RouteConstants.BugReports.Create)
                .To<BugReportsController>(c => c.Create());
        });

        AssertExceptionMessageContains(exception, MethodNotAllowed);
    }
}

Note that I do put my routes in a constants file, so I have to define them once and can use them in all unit and integration tests. This constants file is not shared with the API itself as this would nullify the route URI checks.

I also got a small base class with some helper properties and methods to check on the most common errors and convert one of my objects to JSON.

public abstract class ControllerRouteTestBase
{
    /// <summary>
    /// Routing can't be resolved
    /// </summary>
    protected const string NotFound = "'Not Found'";
    /// <summary>
    /// HttpMethod is incorrect, e.g. GET on POST endpoint
    /// </summary>
    protected const string MethodNotAllowed = "'Method Not Allowed'";
    /// <summary>
    /// Trying to post an incorrect parameter (body)
    /// </summary>
    protected const string ParameterIncorrect = "parameter was different";
    /// <summary>
    /// Routing to an incorrect method
    /// </summary>
    protected const string RoutingIncorrect = "but instead matched";

    protected void AssertExceptionMessageContains(Exception exception, string message)
    {
        Assert.IsTrue(exception.Message.Contains(message), exception.Message);
    }

    protected string GetJson<T>(T entity)
    {
        return JsonConvert.SerializeObject(entity);
    }
}

Data (integration) testing

We’re now quite sure that our API’s endpoints won’t change by accident. The next step is making sure the data doesn’t change it’s contract and we’re getting valid data back. While MyTested.WebApi does support different APIs to test both, we’ve combined them into a single integration test (in the end you can spend 95% of your time writing tests).

First of all, the base class. As MyTested.WebApi starts up an actual WebApi service to hit, we need to do some configuration for the WebApi to work and also run everything in a database transaction so every single test can rollback and leave the database in a valid state. In our project we’re using Automapper, Autofac and Entity Framework that have to be configured to work correctly. MyTested.WebApi easily allows you to hook into the startup.

public class ControllerIntegrationTestBase
{
    protected MyContext DbContext;
    private DbContextTransaction _dbContextTransaction;
    ...

    [SetUp]
    public void Setup()
    {
        CreateMocks();
        var container = ConfigureAutoFac();
        AutomapperConfig.Configure(container);

        DbContext = container.Resolve<MyContext>();
        _dbContextTransaction = DbContext.Database.BeginTransaction(IsolationLevel.ReadUncommitted);

        OnSetup(); // make it possible for each test class to plug in extra logic

        MyWebApi
            .IsRegisteredWith(WebApiConfig.Register)
            .WithDependencyResolver(() => new AutofacWebApiDependencyResolver(container))
            .AndStartsServer();
    }

    [TearDown]
    public void TearDown()
    {
        OnTearDown();

        _dbContextTransaction.Rollback();
        _dbContextTransaction.Dispose();
        DbContext.Dispose();

        MyWebApi.Server().Stops();
    }

    public virtual void OnSetup() {}
    public virtual void OnTearDown() {}

    protected string GetJson<T>(T entity)
    {
        return JsonConvert.SerializeObject(entity);
    }

    private void CreateMocks()
    {
        ...
    }

    private IContainer ConfigureAutoFac()
    {
        var container = AutofacConfig.Configure(MyWebApi.Configuration, typeof(ApiController).Assembly);

        // override default configuration
        var builder = new ContainerBuilder();

        builder.RegisterType<MyContext>().As<MyContext>().SingleInstance();
        builder.RegisterInstance(MyMock.Object).As<IMockedObject>().SingleInstance();
        builder.Update(container);

        return container;
    }
}

Once that is done, we’re ready to write the actual tests. Similar to the route tests, you can use different HTTP verbs, send and receive data etc. You can even add in authentication if that’s needed. Here are a few samples.

[TestFixture]
public partial class ProjectControllerTests : ControllerIntegrationTestBase
{
    [Test]
    public void Projects_GetAll_ShouldReturnData()
    {
        MyWebApi
            .Server()
            .Working()
            .WithHttpRequestMessage(req => req
                .WithRequestUri(RouteConstants.Projects.GetAll + "?limit=10")
                .WithMethod(HttpMethod.Get))
            .ShouldReturnHttpResponseMessage()
            .WithStatusCode(HttpStatusCode.OK)
            .WithResponseModelOfType<PagedSortedResponse<PagedList<ProjectForList>>>()
            .Passing(r => r.Data.Count > 0);
    }

    [Test]
    public void Projects_GetAll_WithoutPaging_ShouldReturnError()
    {
        MyWebApi
            .Server()
            .Working()
            .WithHttpRequestMessage(req => req
                .WithRequestUri(RouteConstants.Projects.GetAll)
                .WithMethod(HttpMethod.Get))
            .ShouldReturnHttpResponseMessage()
            .WithStatusCode(HttpStatusCode.BadRequest);
    }

    [Test]
    public void Projects_CreateInvalidModel_ShouldReturnError()
    {
        MyWebApi
            .Server()
            .Working()
            .WithHttpRequestMessage(req => req
                .WithRequestUri(RouteConstants.Projects.Create)
                .WithMethod(HttpMethod.Post)
                .WithJsonContent(GetJson(new ProjectBuilder().AsNew(SeededProjectName).Build())))
            .ShouldReturnHttpResponseMessage()
            .WithStatusCode(HttpStatusCode.BadRequest)
            .WithStringContent((str) => str.Contains("Name not available"));
    }

    /// <summary>
    /// Uses the Create entrypoint to insert a test project and return the Id to receive the item again
    /// </summary>
    private int CreateTestProject(string name)
    {
        int projectId = -1;

        // create by name
        MyWebApi
            .Server()
            .Working()
            .WithHttpRequestMessage(req => req
                .WithRequestUri(RouteConstants.Projects.Create)
                .WithMethod(HttpMethod.Post)
                .WithJsonContent(GetJson(new ProjectBuilder().AsNew(name).Build())))
            .ShouldReturnHttpResponseMessage()
            .WithStatusCode(HttpStatusCode.Created)
            .WithResponseModelOfType<SaveResult<Project>>()
            .Passing(r =>
            {
                projectId = r?.Model?.Id ?? -1;
                return r?.Model != null;
            });

        return projectId;
    }
}

It’s up to you to decide what you want to test and what not, but at least I’ll be confident that my changes in the backend won’t break the frontend or that anyone else on the team does so. This is only a small portion of what this wonderful open-source library can do, but hopefully it sparked your interest. Don’t hesitate to check it out.

In the code above I’ve simply created a new object, but typically I’ll be using a builder pattern for this. More precisely a combination of ObjectMother and TestDataBuilder, quite similar to the CustomerBuilder class at the end of this post.

Comments

comments powered by Disqus