January 17, 2017

Writing Web API controller tests

I am fan of TDD principle, and always trying to write tests first. Today I want to show, how I usually test Web API controller. There are two approaches:

1. Unit test
Consider UserController class, which has POST method that registers users.
public class UserController : ApiController
{
    private readonly IRegistrationService registrationService;

    public UserController(IRegistrationService registrationService)
    {
        this.registrationService = registrationService;
    }

    public HttpResponseMessage Post([FromBody]string json)
    {
        var model = new JavaScriptSerializer().Deserialize<RegistrationModel>(json);

        bool success = this.registrationService.Register(model);

        return new HttpResponseMessage(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError);
    }
}
And test (which, of course, was written first)
[Test]
public void TestUserCanBeRegistered()
{
    // Arrange
    var registrationService = new Mock<IRegistrationService>();
    registrationService
        .Setup(x => x.Register(It.IsAny<RegistrationModel>()))
        .Returns(true);

    var userController = new UserController(registrationService.Object);

    // Act
    var response = userController.Post(@"{""login"":""Alex"", ""password"": ""password""}");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

Test runs OK
BUT if you try to send real request through the fiddler, for example, you will get an error.

Content-Type: application/json; charset=utf-8
Content-Length: 40
{"login":"Alex", "password": "password"}

HTTP/1.1 500 Internal Server Error
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RTpcVGVtcFxXZWJBcHBcV2ViQXBwXHVzZXJc?=
X-Powered-By: ASP.NET
Date: Thu, 12 Jan 2017 12:57:07 GMT
Content-Length: 2339
{"Message":"An error has occurred.","ExceptionMessage":"Value cannot be null.\r\nParameter name: input","ExceptionType":"System.ArgumentNullException","StackTrace":"   at System.Web.Script.Serialization.JavaScriptSerializer.Deserialize(JavaScriptSerializer serializer, String input, Type type, Int32 depthLimit)

How we can get it, if test passed OK? This happens, because we fill method parameters by ourselves, not by real request. Error in tests costs much more than in the code, that's why I prefer another method of controller testing

2. Integration test*
Let’s test it in the other manner.
First, we need to add some references to our project with tests.

Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.AspNet.WebApi.Owin
Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Testing
Install-Package Unity

For our convenience we create base class for integration tests
internal class IntegrationTestBase
{
    protected TestServer Server { get; private set; }

    [TestFixtureSetUp]
    public void BindServices()
    {
        this.Server = TestServer.Create<TestStartup>();
    }

    [TestFixtureTearDown]
    public void DisposeServer()
    {
        this.Server.Dispose();
    }
}

TestStartup.cs. If you are hosting your application with OWIN you can use Startup.cs here.
public class TestStartup
{
    public void Configuration(IAppBuilder builder)
    {
        var config = new HttpConfiguration();

        config.Routes.MapHttpRoute("users", "users", new { controller = "User" });

        var container = new UnityContainer();
        container.RegisterType<IRegistrationService, RegistrationService>();
        config.DependencyResolver = new UnityResolver(container);

        builder.UseWebApi(config);
    }
}

And finally write new test using OWIN TestServer that will be hosted InProc.
[Test]
public async void TestRequestWasMappedCorrectlyAndUserRegistered()
{
    // Arrange
    using (var client = new HttpClient(this.Server.Handler))
    {
        var body = new StringContent(@"{""login"":""Alex"", ""password"": ""password""}", Encoding.UTF8, "application/json");

        // Act
        var response = await client.PostAsync("http://api.webapp.com/users/", body);
        // Assert
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}









This approach simulates real request to the API resource from the client. Test didn’t pass. Now this correlates with the real behavior. Time to “green” this test. There are many ways to do it:

1. Change type of the input parameter to HttpRequestMessage
public HttpResponseMessage Post(HttpRequestMessage request) { … }
and then gets the content
string body = message.Content.ReadAsStringAsync().Result;

2. Change type of the input parameter to IDictionary<string, string> (using IDictionary<string, object> is not recommended)
public HttpResponseMessage Post(IDictionary<string, string> dict) { … }

3. Or specify concrete type RegistrationModel
public HttpResponseMessage Post(RegistrationModel model) { … }

4. Or vice versa make it more generic with dynamic
public HttpResponseMessage Post([FromBody]dynamic obj) { … }

I chose third variant, so our controller method will look like
public HttpResponseMessage Post(RegistrationModel model)
{
    bool success = this.registrationService.Register(model);

    return new HttpResponseMessage(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError);
}
Now all tests pass OK (of course we should fix test ‘TestUserCanBeRegistered’, because we changed the contract).







Example of the code available on github

* - I name it integration, because it checks not only controller class but also MediaTypeFormatter

No comments:

Post a Comment