Quantcast
Channel: The Problem Solver
Viewing all articles
Browse latest Browse all 57

Unit testing code depending on the ASP.NET WebApi HttpClient

$
0
0

In a previous post I showed how to unit test an ASP.NET WebAPI Controller. But with a REST service there is both a client and a service component. Assuming for a moment the client part is also written in C# we should test that as well.

In this case the client application contains the following class to load books from the REST WebAPI controller:

   1:publicclass BooksClient
   2: {
   3:privatereadonly HttpClient _httpClient;
   4:  
   5:public BooksClient(HttpClient httpClient)
   6:     {
   7:         _httpClient = httpClient;
   8:         BaseUrl = new Uri("http://localhost:63895/api/books/");
   9:     }
  10:  
  11:public Uri BaseUrl { get; private set; }
  12:  
  13:public IEnumerable<Book> GetBooks()
  14:     {
  15:         var response = _httpClient.GetAsync(BaseUrl).Result;
  16:         response.EnsureSuccessStatusCode();
  17:return response.Content.ReadAsAsync<Book[]>().Result;
  18:     }
  19:  
  20:public Book GetBook(int id)
  21:     {
  22:         var requestUri = new Uri(BaseUrl, id.ToString(CultureInfo.InvariantCulture));
  23:         var response = _httpClient.GetAsync(requestUri).Result;
  24:         response.EnsureSuccessStatusCode();
  25:return response.Content.ReadAsAsync<Book>().Result;
  26:     }
  27:  
  28:public Tuple<Book, Uri> PostBook(Book book)
  29:     {
  30:         var response = _httpClient.PostAsJsonAsync(BaseUrl.ToString(), book).Result;
  31:         response.EnsureSuccessStatusCode();
  32:         var newBook = response.Content.ReadAsAsync<Book>().Result;
  33:         var location = response.Headers.Location;
  34:returnnew Tuple<Book, Uri>(newBook, location);
  35:     }
  36: }

This class uses the HttpClient to request the data from the service and extracts the books from the body before returning them.

 

Testing the BooksClient class

If we want to test this class we need to pass in an HttpClient object. This might not sound like a big deal but as this class doesn’t implement an interface we can’t use most of the standard mocking frameworks like Moq to replace the HttpClient with a test fake.

It turns out that this isn’t a big issue though as you can replace the internal pipeline of the HttpClient instead. This is done by passing in an HttpMessageHandler. This HttpMessageHandler is used as the pipeline to send requests and we can completely replace this with our own implementation. The easiest way is by creating a dummy DelegatingHandler and overriding the SendAsync() function to just return a fake response instead of actually doing an HTTP request.

 

The TestingDelegatingHandler<T> class

Creating a dummy DelegatingHandler  isn’t hard but using the TestingDelegatingHandler<T> makes this really easy. The complete code is below and is pretty simple:

   1:publicclass TestingDelegatingHandler<T> : DelegatingHandler
   2: {
   3:private Func<HttpRequestMessage, HttpResponseMessage> _httpResponseMessageFunc;
   4:  
   5:public TestingDelegatingHandler(T value)
   6:         : this(HttpStatusCode.OK, value)
   7:     { }
   8:  
   9:public TestingDelegatingHandler(HttpStatusCode statusCode)
  10:         : this(statusCode, default(T))
  11:     { }
  12:  
  13:public TestingDelegatingHandler(HttpStatusCode statusCode, T value)
  14:     {
  15:         _httpResponseMessageFunc = request => request.CreateResponse(statusCode, value);
  16:     }
  17:  
  18:public TestingDelegatingHandler(
  19:         Func<HttpRequestMessage, HttpResponseMessage> httpResponseMessageFunc)
  20:     {
  21:         _httpResponseMessageFunc = httpResponseMessageFunc;
  22:     }
  23:  
  24:protectedoverride Task<HttpResponseMessage> SendAsync(
  25:         HttpRequestMessage request, CancellationToken cancellationToken)
  26:     {
  27:return Task.Factory.StartNew(() => _httpResponseMessageFunc(request));
  28:     }
  29: }

The most important function is the SendAsync() which returns a new Task. Did I mention that the WebAPI is completely async enabled? Well it is so we just have to return a new Task that returns the HttpResponseMessage instead of the HttpResponseMessage directly.


Testing the GetBooks() function

The GetBooks() function gets all books from the REST service. A test is pretty simple. The only thing to be aware of is that in order to use the TestingDelegatingHandler we also need to create an HttpServer object and pass in an HttpConfiguration object. Normally an empty HttpConfiguration will be enough.

   1: [TestMethod]
   2:publicvoid WhenGettingAllBooksTheyShouldBeReturned()
   3: {
   4:// Arrange
   5:     var books = new[]
   6:     {
   7:new Book{Id = 1, Author = "Me", Title = "Book 1"},
   8:new Book{Id = 2, Author = "You", Title = "Book 2"}
   9:     };
  10:     var testingHandler = new TestingDelegatingHandler<Book[]>(books);
  11:     var server = new HttpServer(new HttpConfiguration(), testingHandler);
  12:     var client = new BooksClient(new HttpClient(server));
  13:  
  14:// Act
  15:     var booksReturned = client.GetBooks();
  16:  
  17:// Assert
  18:     Assert.AreEqual(2, booksReturned.Count());
  19: }


Simple enough right?

 

Testing the GetBook(int id) function

Testing this method is not much harder but we need to test both a positive and a negative result. The positive is just as simple as above:

   1: [TestMethod]
   2:publicvoid WhenGettingAValidBookItShouldBeReturned()
   3: {
   4:// Arrange
   5:     var book = new Book { Id = 2, Author = "You", Title = "Book 2" };
   6:     var testingHandler = new TestingDelegatingHandler<Book>(book);
   7:     var server = new HttpServer(new HttpConfiguration(), testingHandler);
   8:     var client = new BooksClient(new HttpClient(server));
   9:  
  10:// Act
  11:     var bookReturned = client.GetBook(2);
  12:  
  13:// Assert
  14:     Assert.IsNotNull(bookReturned);
  15:     Assert.AreEqual("Book 2", bookReturned.Title);
  16: }

 

The negative case isn’t much harder, all we need to do is make sure our dummy service returns an HTTP 404 Not Found status. With the overloads for the TestingDelegatingHandler<T> this is easy enough. See below:

   1: [TestMethod]
   2: [ExpectedException(typeof(HttpRequestException))]
   3:publicvoid WhenGettingAnInvalidBookItShouldThrow()
   4: {
   5:// Arrange
   6:     var testingHandler = new TestingDelegatingHandler<Book>(HttpStatusCode.NotFound);
   7:     var server = new HttpServer(new HttpConfiguration(), testingHandler);
   8:     var client = new BooksClient(new HttpClient(server));
   9:  
  10:// Act
  11:     client.GetBook(-1);
  12:  
  13:// Assert
  14:     Assert.Fail();
  15: }

 

Nice and simple right?

 

Testing am HTTP POST action

Testing an HTTP POST action to add a new book is slightly more complex. Not a whole lot but the REST convention is to return both an HTTP 201 Created status as well as the location of the new resource in an HTTP header. For this purpose the TestingDelegatingHandler<T> has an overload where you can just pass in a lambda to create the response. This gives us full flexibility and with that the test is simple enough.

   1: [TestMethod]
   2:publicvoid WhenPostingABookItShouldBeAdded()
   3: {
   4:// Arrange
   5:     var book = new Book { Id = 2, Author = "You", Title = "Book 2" };
   6:     var testingHandler = new TestingDelegatingHandler<Book>(request =>
   7:     {
   8:         var response = request.CreateResponse(HttpStatusCode.Created, book);
   9:         response.Headers.Location =
  10:new Uri(string.Format("http://domain.com/api/books/{0}", book.Id));
  11:return response;
  12:     });
  13:     var server = new HttpServer(new HttpConfiguration(), testingHandler);
  14:     var client = new BooksClient(new HttpClient(server));
  15:  
  16:// Act
  17:     var result = client.PostBook(new Book());
  18:  
  19:// Assert
  20:     var bookReturned = result.Item1;
  21:     Assert.IsNotNull(bookReturned);
  22:     Assert.AreEqual("Book 2", bookReturned.Title);
  23:  
  24:     var location = result.Item2;
  25:     Assert.AreEqual(new Uri("http://domain.com/api/books/2"), location);
  26: }

 

Of course we still need tests for updating existing resources as well as deleting them but with these examples those should be easy enough :-)

 

Enjoy!


Viewing all articles
Browse latest Browse all 57

Trending Articles