Chapter 17. Testability
The problem with troubleshooting is that trouble shoots back.
As developers, we often find ourselves in situations where we spend a significant amount of time trying to troubleshoot issues that might occur in our Web API implementation. In many cases, we just use a trial-and-error method with a browser or an HTTP debugger, but this sort of manual testing is time-consuming, irreproducible, and error-prone. To make things worse, as the number of scenarios that our Web API can cover increases, manually testing every possible path becomes a daunting task.
Over the years, different tools and practices have emerged to improve our lives as developers. Automated testing, for example, is one of the areas in which a lot of improvement has been made. Automated testing in this context consists of creating or configuring a piece of software to perform testing for us. There are some obvious advantages with this approach: a test becomes reproducible, and it can be run at any time, or we can even schedule it to run automatically with no interaction whatsoever.
As part of this chapter, we will explore the two most popular choices for developers to automate software testing in ASP.NET Web API: unit testing and integration testing. For those who are not familiar with these concepts, we have included a brief introduction to the subject, which also mentions test-driven development (TDD) as a core practice. Since this could be a very extensive subject, we have constrained it to ASP.NET Web API and how you can leverage it for testing all the components you build with the framework.
Unit Tests
A unit test is code that we typically write for verifying the expected behavior of some other code in isolation. We start the process of writing unit tests by splitting the application code into discrete parts, such as class methods, that are easy to manage and can be tested in isolation without affecting one another. For example, in the context of ASP.NET Web API, you might want to write different unit tests for every public method exposed in an ApiController
implementation. “In isolation” means that there is no dependency between tests, so the order in which they run should not alter the final result. In fact, you should be able to run all the unit tests simultaneously.
A unit test is typically organized into three parts:
- Arrange: set one or more objects in a known state.
- Act: manipulate the state of those objects by calling some methods on it, for example.
- Assert: check the results of the tests and compare them against an expected outcome.
As with any piece of source code, unit tests should also be treated as a development artifact that can be stored in a source repository. By having the tests in a repository, you can use them for multiple purposes such as documenting the expected behavior of certain code or ensuring that behavior is still correct as different changes in the implementation are made. They should also be easy and fast to run; otherwise, developers would be less likely to run them.
Unit Testing Frameworks
Unit testing frameworks help simplify the process of writing unit tests by enforcing certain aspects of the test structure and also providing tools to run them. As with any framework, they are not something strictly necessary, but they can certainly speed up the process of getting the unit tests done and working.
The most common unit testing frameworks used nowadays by developers are typically part of the xUnit family, including the Visual Studio unit testing tool (which is included in the paid version of Visual Studio), or xUnit.Net, a open source initiative that we will use in this chapter to cover both integration testing and unit testing. Most of the frameworks in the xUnit family are either a direct port of JUnit or use some of its concepts or ideas, which were initially originated and became popular in extreme programming.
Getting Started with Unit Testing in Visual Studio
To make things easier for developers, the ASP.NET team has included unit testing support in the New Project dialog in Visual Studio for ASP.NET Web API applications, as shown in Figure 17-1.
By selecting the Create Unit Test project checkbox, you are instructing the Visual Studio project wizard to include a new Unit Test project using the testing framework of your preference (the Visual Studio Unit Testing tool is the only one available by default). When you select the Visual Studio Unit Testing tool, Visual Studio will also generate a project with a set of unit tests for the ASP.NET Web API controllers included in the default template. For other testing tools, this behavior will change based on the project template definition registered in Visual Studio for that tool.
In the case of the ASP.NET Web API project template, which includes a ValuesController
, you will also find the unit test counterpart in the testing project, ValuesControllerTest
. The code in Example 17-1 illustrates the Get
method generated by Visual Studio in that class.
[TestClass]
public
class
ValuesControllerTest
{
[TestMethod]
public
void
Get
()
{
// Arrange
ValuesController
controller
=
new
ValuesController
();
// <1>
// Act
IEnumerable
<
string
>
result
=
controller
.
Get
();
// <2>
// Assert // <3>
Assert
.
IsNotNull
(
result
);
Assert
.
AreEqual
(
2
,
result
.
Count
());
Assert
.
AreEqual
(
"value1"
,
result
.
ElementAt
(
0
));
Assert
.
AreEqual
(
"value2"
,
result
.
ElementAt
(
1
));
}
}
As you can see, this method is organized into Arrange, Act, and Assert parts, as we previously discussed. In the Arrange part <1>
, the ValuesController
under test is instantiated, follow by the Get
method invocation as part of the Act <2>
and the expected values assertions in the Assert
part <3>
.
The Assert
class, as well as the TestClass
and TestMethod
attributes used by this unit test, are part of the Visual Studio unit testing framework. You will typically find these three with different names but similar functionality in any framework from the xUnit family.
While Example 17-1 shows how to unit test a particular controller, you will also find yourself writing unit tests for the rest of the components, such as the ones encapsulating data access or business logic as well as others that make sense only in the context of a Web API like message handlers.
xUnit.NET
xUnit.NET is another alternative in the xUnit family that began as an open source initiative from Brad Wilson and James Newkirk, also one of the authors of NUnit, the first port of JUnit in the .NET world. This framework was conceived with the goal of applying many of the best practices and lessons learned from previous experiences in unit testing and better aligning with the recent changes in the .NET platform. For example, this framework provides a nice way to check exceptions in a test compared to the other traditional frameworks. While most frameworks handle this scenario with the use of attributes, xUnit.NET uses delegates, as shown in Example 17-2.
[TestClass]
public
class
ValuesControllerTest
{
[TestMethod]
public
void
Get
()
{
// Arrange
ValuesController
controller
=
new
ValuesController
();
// Assert // <1>
controller
.
Throws
<
HttpException
>(()
=>
controller
.
Get
(
"bad"
))
// <2>
}
}
As you can see in assert_throws
, the Throws
method provides a simple way to express the Assert <1>
and Act <2>
sections in a single line, where the delegate being passed to this method is responsible for throwing the exception.
Unit test organization
As a rule of thumb, a unit test should test only a single functionality or behavior; otherwise, it would be very complicated to get real feedback about what specifically was wrong. In addition, the unit test by itself wouldn’t provide any value because it would be hard to determine what the expected behavior should be. For that reason, unit tests are typically organized in methods that provide fine-grained feedback. Each method should demonstrate only one expected behavior, but it might do so by using one or more assertions. In the case of xUnit.net, these methods must be decorated with a Fact
attribute to identify them as unit tests. You might also want to organize unit tests in groups using different criteria, such as unit tests for a specific component or for a specific use case. xUnit just uses classes for grouping all the tests together in what is typically called a test suite in unit testing jargon.
The Assert class
Most xUnit frameworks, including xUnit.net, use an Assert
class with common static methods for doing comparisons or checking results using a fluent interface, which helps to express intent explicitly. For example, if you express that the returning value of a method call should not be null
, Assert.IsNotNull(result)
probably communicates that intent better than Assert.IsTrue(result == null)
, but this is just a personal preference of the developer writing the tests.
All the methods in this Assert
class throw exceptions when the condition evaluates to false
, making it possible to detect whether a unit test failed.
The Role of Unit Testing in Test-Driven Development
Test-driven development (TDD) is a design technique in which you use unit tests to drive the design of production code by writing the tests first, followed by the necessary application code to make the tests pass. When TDD is applied correctly, the resulting artifacts are the application code along with the unit tests describing the expected behavior, which you can also use at any time later to make sure the behavior of the production code is still correct. However, TDD is focused not only on using unit tests to reduce the number of bugs in the production code, but also in improving that code’s design. Since you are writing unit tests first, you are describing how the production code should behave before it is actually written. You are writing the application code that you need, and only that, with no chance of writing any unnecessary implementation.
A common mistake is to assume that writing some unit tests implies TDD. While TDD requires unit tests for driving the design of your code, the opposite is not necessarily true. You can write unit tests after the code is written, which is typically done for increasing the percentage of code that you have covered with tests, but that does not mean you are using TDD, as your application code already exists.
The red and green cycle
When you are writing unit tests, the words red and green can be used as a replacement for failure and success, respectively. These are the colors most test runners also use to help developers quickly identify which tests are passing or failing. TDD also makes extensive use of these two colors for driving the development of new functionality. Because you write a test for code that does not exist yet, your first test run will fail. You then write code designed to pass the test and rerun the test. You will get a green light if the behavior of the production code is correct, a red one if your production code still needs some work. In that way, you are constantly moving through a red/green cycle. However, once a test passes, you should stop writing new production code until you have a new test, with new requirements, that fails. You basically write enough application code to make the test pass, which might not be perfect at first glance, but you have a tool to verify that any improvement made to the production code does not affect the expected behavior. If you want to improve or optimize certain aspects of the production code, you can do it as long as you don’t modify the public interface of your components. The tests can still be used as backup to make sure the underlying behavior is still the same.
Code refactoring
The term code refactoring refers to the act of changing the internal implementation of a component while leaving its observable behavior intact—for example, reducing a large public method to a set of individually concise and single-purpose methods, which are easier to maintain. When you are refactoring your production code, you don’t modify the existing unit tests, which would imply you are adding, removing, or modifying existing functionality. The existing unit tests are used to make sure the observable behavior of the application code is still the same after the refactoring. If you have, for example, an action in a controller that returns a list of customers and a unit test for verifying that behavior, that test will only expect to receive the same list of customers no matter how the internal implementation is getting that list. If you detect some inconsistencies in that code, you can use refactoring to improve it and use the existing unit tests to make sure nothing was broken with the introduced changes.
Example 17-3 shows some code that can be improved through internal refactoring.
public abstract class IssueSource : IIssueSource { HttpMessageHandler _handler = null; protected IssueSource(HttpMessageHandler handler = null) { _handler = handler; } public virtual Task<IEnumerable<Issue>> FindAsync() { HttpClient client; if (_handler != null) client = new HttpClient(_handler); else client = new HttpClient(); // Do something with the HttpClient instance ... } public virtual Task<IEnumerable<Issue>> FindAsyncQuery(dynamic values) { HttpClient client; if (_handler != null) client = new HttpClient(_handler); else client = new HttpClient(); // Do something with the HttpClient instance ... } }
Both methods are initializing a new HttpClient
instance. If that code requires some changes, such as the addition of new settings, it has to be changed in the two methods. That code can be moved to a common method, which is kept internal to the implementation, as shown in Example 17-4.
public abstract class IssueSource : IIssueSource { HttpMessageHandler _handler = null; protected IssueSource(HttpMessageHandler handler = null) { _handler = handler; } public virtual Task<IEnumerable<Issue>> FindAsync() { HttpClient client = GetClient(); // Do something with the HttpClient instance ... } public virtual Task<IEnumerable<Issue>> FindAsyncQuery(dynamic values) { HttpClient client = GetClient(); // Do something with the HttpClient instance ... } protected HttpClient GetClient() { HttpClient client; if (_handler != null) client = new HttpClient(_handler); else client = new HttpClient(); return client; } }
We’ve removed the duplicated code without modifying the external interface of this class. The expected behavior is still the same, so the unit tests don’t have to be changed, and they can be used to verify this change did not break anything.
Dependency injection and mocking
Dependency injection is another practice that usually goes hand in hand with unit testing in static languages where depedencies cannot be easily replaced at runtime. Since unit testing focuses on testing the behavior of certain code in isolation, you would want to minimize the impact of any external dependency during testing. By using dependency injection, you replace all the hardcoded dependencies and inject them at runtime, so their behavior can be faked. For example, if a Web API controller relies on a data access class for querying a database, we will not want to unit test the controller with that explicit dependency. That would imply a database is initialized and ready to be used every time the test is run, which might not always be the case. The controller is first decoupled from the real data access class implementation via an interface or an abstract class, which is later injected into the controller through either an argument in the constructor or a property setter. As part of the unit test, the only pending task is to create a fake class that implements the interface or abstract class and also satisfies the requirements of the test. That fake class could simply mimic the same interface and return the expected data for the test from a memory list previously initialized in the same test. In that way, we get rid of the database dependency in the tests. There are multiple open source frameworks, such as Moq or RhinoMocks, for automatically generating a fake or mock from an interface or base class, and setting expectations for how that mock class should be behave. Example 17-5 shows a fragment of a unit test for our Issue Tracker Web API that instantiates a mock (from the Moq framework) for emulating the behavior of a data access class.
public
class
IssuesControllerTests
{
private
Mock
<
IIssueSource
>
_mockIssueSource
=
new
Mock
<
IIssueSource
>();
// <1>
private
IssuesController
_controller
;
public
IssuesControllerTests
()
{
_controller
=
new
IssuesController
(
_mockIssueSource
.
Object
);
// <2>
}
[Fact]
public
void
ShouldCallFindAsyncWhenGETForAllIssues
()
{
_controller
.
Get
();
_mockIssueSource
.
Verify
(
i
=>
i
.
FindAsync
());
// <3>
}
}
A mock for the IIssueSource
interface is instantiated via the Mock
class provided by the Moq framework <1>
and injected into the IssuesController
constructor <2>
. The unit test invokes the Get
method on the controller and verifies that the FindAsync
method on the Mock
object was actually called <3>
. Verify
is a method also provided by the Moq framework for checking if a method was invoked or not, and how many times it was invoked (invoking the method FindAsync
more than once would imply a bug in the code, for example). If this framework isn’t used, a lot of manual and repetitive code would be needed to implement a similar functionality.
Unit Testing an ASP.NET Web API Implementation
There are several components in an ASP.NET Web API implementation that you will want to test in isolation. As part of this chapter, we will cover some of them, such as ApiController
, MediaTypeFormatter
, and HttpMessageHandler
. Toward the end of the chapter, we will also explore the idea of using the HttpClient class and in-memory hosting for doing integration testing.
Unit Testing an ApiController
An ApiController
in the context of ASP.NET Web API acts as an entry point to your Web API implementation. It serves as a bridge for exposing application logic to the external world via HTTP. One of the main things you will want to test is how the controller reacts to different request messages in isolation. You can pick which messages to use based on the supported scenarios or use cases for the Web API. Isolation is also a key aspect for unit testing, as you cannot assume that everything is correctly set up in the Web API runtime before running the tests. For example, if your ApiController
relies on the authenticated user for doing certain operations, the configuration of that user should also be done within the unit tests. The same idea applies for the initialization of the request messages.
As part of this section, we will use the ApiController
that we built for managing issues as the starting point. We will try to write unit tests against that code for covering some of the supported use cases. Let’s start with Example 17-6.
public
class
IssuesController
:
ApiController
{
private
readonly
IIssueSource
_issueSource
;
public
IssuesController
(
IIssueSource
issueSource
)
{
_issueSource
=
issueSource
;
}
public
async
Task
<
Issue
>
Get
(
string
id
)
// <1>
{
var
issue
=
await
_issueSource
.
FindAsync
(
id
);
if
(
issue
==
null
)
throw
new
HttpResponseException
(
HttpStatusCode
.
NotFound
);
return
issue
;
}
public
async
Task
<
HttpResponseMessage
>
Post
(
Issue
issue
)
// <2>
{
var
createdIssue
=
await
_issueSource
.
CreateAsync
(
issue
);
var
link
=
Url
.
Link
(
"DefaultApi"
,
new
{
Controller
=
"issues"
,
id
=
createdIssue
.
Id
});
var
response
=
Request
.
CreateResponse
(
HttpStatusCode
.
Created
,
createdIssue
);
response
.
Headers
.
Location
=
new
Uri
(
link
);
return
response
;
}
}
Example 17-6 shows our first implementation of the IssuesController
, which does not look too complex at first glance. It contains a Get
method for retrieving an existing issue <1>
, and a Post
method for adding a new issue <2>
. It also depends on an IIssueSource
instance for handling persistence concerns.
Testing the Get method
Our first Get
method, shown in Example 17-7, looks very simple and returns an existing issue by delegating the call to the IIssueSource
implementation. As the unit tests should not rely on a concrete IIssueSource
implementation, we will use a Mock
instead.
public
class
IssuesControllerTests
{
private
Mock
<
IIssueSource
>
_mockIssueSource
=
new
Mock
<
IIssueSource
>();
private
IssuesController
_controller
;
public
IssuesControllerTests
()
{
_controller
=
new
IssuesController
(
_mockIssueSource
.
Object
);
// <1>
}
[Fact]
public
void
ShouldReturnIssueWhenGETForExistingIssue
()
{
var
issue
=
new
Issue
();
_mockIssueSource
.
Setup
(
i
=>
i
.
FindAsync
(
"1"
))
.
Returns
(
Task
.
FromResult
(
issue
));
// <2>
var
foundIssue
=
_controller
.
Get
(
"1"
).
Result
;
// <3>
Assert
.
Equal
(
issue
,
foundIssue
);
// <4>
}
}
Example 17-7 mainly checks that the controller invokes the FindAsync
method in the IIssueSource
instance to return an existing issue. The controller is first initialized with a mock instance of IIssueSource
<1>
as part of the test initialization. That mock instance is set up to return an issue when it receives an argument equal to “1” <2>
, which is the same argument that gets passed to the Get
method controller <3>
. Finally, the issue returned by the controller is compared with the issue injected in the mock to make sure they are the same <4>
.
That’s the happy path when an issue is returned by the IIssueSource
implementation, but we will also want to test how the controller reacts when a requested issue is not found. We will create a new test for verifying that scenario, as shown in Example 17-8.
[Fact]
public
void
ShouldReturnNotFoundWhenGETForNonExistingIssue
()
{
_mockIssueSource
.
Setup
(
i
=>
i
.
FindAsync
(
"1"
))
.
Returns
(
Task
.
FromResult
((
Issue
)
null
));
// <1>
var
ex
=
Assert
.
Throws
<
AggregateException
>(()
=>
{
var
task
=
_controller
.
Get
(
"1"
);
var
result
=
task
.
Result
;
});
// <2>
Assert
.
IsType
<
HttpResponseException
>(
ex
.
InnerException
);
// <3>
Assert
.
Equal
(
HttpStatusCode
.
NotFound
,
((
HttpResponseException
)
ex
.
InnerException
).
Response
.
StatusCode
);
// <4>
}
The controller is throwing an HttpException
with a status code equal to 404
when the issue is not found. The unit test is initializing the Mock
for returning a Task
with a null
result when the issue ID is equal to 1
. As part of the Assert section in the unit test, we are using the Throws
method (provided in xUnit.NET) in the Assert
class for checking whether a method returns an exception or not <2>
. The Throws
method receives a delegate that might throw an exception, and it tries to capture it. Finally, we are checking if the thrown exception is of the type HttpResponseException
<3>
and the status code on that exception is set to 404
<4>
.
Testing the Post method
The Post
method in the controller will create a new issue. We need to verify that the issue is correctly passed to the IIssuesSource
implementation, and also that the response headers are correctly set before leaving the controller. Since this controller method relies on the HttpRequestMessage
and UrlHelper
instances in the controller context for generating a response and the link to the new resource, some tedious work is required to initialize the runtime configuration and the routing table, as shown in Example 17-9.
_controller
.
Configuration
=
new
HttpConfiguration
();
// <1>
var
route
=
_controller
.
Configuration
.
Routes
.
MapHttpRoute
(
name
:
"DefaultApi"
,
routeTemplate
:
"api/{controller}/{id}"
,
defaults
:
new
{
id
=
RouteParameter
.
Optional
}
);
// <2>
var
routeData
=
new
HttpRouteData
(
route
,
new
HttpRouteValueDictionary
{
{
"controller"
,
"Issues"
}
}
);
_controller
.
Request
=
new
HttpRequestMessage
(
HttpMethod
.
Post
,
"http://test.com/issues"
);
// <3>
_controller
.
Request
.
Properties
.
Add
(
HttpPropertyKeys
.
HttpConfigurationKey
,
controller
.
Configuration
);
_controller
.
Request
.
Properties
.
Add
(
HttpPropertyKeys
.
HttpRouteDataKey
,
routeData
);
// <4>
A new HttpConfiguration
object is instantiated and set in the controller instance <1>
. The route is set up in <2>
and added to the existing configuration object. A new request object is created and set up with the HTTP verb and URI expected by the test <3>
. Finally, the routing data and configuration object are associated with the request object through the generic property bag Properties
, which is the one used by UrlHelper
to look up these objects <4>
.
The ASP.NET Web API team already simplified this scenario in Web API, as we will see in the next section. In the meantime, if you are still using the first Web API release, there is a set of extension methods provided as part of the WebApiContrib project, which configures the controller with a single line of code (see Example 17-10).
public
static
class
ApiControllerExtensions
{
public
static
void
ConfigureForTesting
(
this
ApiController
controller
,
HttpRequestMessage
request
,
string
routeName
=
null
,
IHttpRoute
route
=
null
);
public
static
void
ConfigureForTesting
(
this
ApiController
controller
,
HttpMethod
method
,
string
uri
,
string
routeName
=
null
,
IHttpRoute
route
=
null
);
}
These extension methods receive the request instance or HTTP method to use, and optionally a URL or default route to use in the test. We will be using these extension methods in the rest of the chapter for simplifying the code in the tests. Our first unit test is shown in Example 17-11.
[Fact]
public
void
ShouldCallCreateAsyncWhenPOSTForNewIssue
()
{
//Arrange
_controller
.
ConfigureForTesting
(
HttpMethod
.
Post
,
"http://test.com/issues"
);
// <1>
var
issue
=
new
Issue
();
_mockIssueSource
.
Setup
(
i
=>
i
.
CreateAsync
(
issue
))
.
Returns
(()
=>
Task
.
FromResult
(
issue
));
// <2>
//Act
var
response
=
_controller
.
Post
(
issue
).
Result
;
// <3>
//Assert
_mockIssueSource
.
Verify
(
i
=>
i
.
CreateAsync
(
It
.
Is
<
Issue
>(
iss
=>
iss
.
Equals
(
issue
))));
// <4>
}
The HttpRequestMessage
and the UrlHelper
instances in the IssuesController
are initialized with the extension method ConfigureForTesting
instances in the instances in the <1>
.
Once the controller is initialized, the IIssueSource
mock instance is set to return an asynchronous task, which emulates the work of persisting the issue in the backend <2>
.
The Post
method in the controller is invoked with a new issue <3>
.
The test verifies that the CreateAsync
method in the mock instance was actually invoked <4>
.
An additional test is needed to verify that a valid response is returned after we invoke the CreateAsync
method in the IIssueSource
implementation Example 17-12.
[Fact]
public
void
ShouldSetResponseHeadersWhenPOSTForNewIssue
()
{
//Arrange
_controller
.
ConfigureForTesting
(
HttpMethod
.
Post
,
"http://test.com/issues"
);
var
createdIssue
=
new
Issue
();
createdIssue
.
Id
=
"1"
;
_mockIssueSource
.
Setup
(
i
=>
i
.
CreateAsync
(
createdIssue
)).
Returns
(()
=>
Task
.
FromResult
(
createdIssue
));
// <1>
//Act
var
response
=
_controller
.
Post
(
createdIssue
).
Result
;
// <2>
//Assert
response
.
StatusCode
.
ShouldEqual
(
HttpStatusCode
.
Created
);
// <3>
response
.
Headers
.
Location
.
AbsoluteUri
.
ShouldEqual
(
"http://test.com/issues/1"
);
}
The IIssueSource
mock instance is set up to return a task with the created issue <1>
.
The created issue is passed as an argument to the controller instance <2>
.
The test verifies that the expected HTTP status code is equal to Created
and the new resource location is http://test.com/issues/1.
At this point, you should have a pretty good idea of what is involved in unit testing a controller in ASP.NET Web API. In the next sections, we will discuss what needs to be done for testing a MediaTypeFormatter
and an HttpMessageHandler
<3>
.
IHttpActionResult in Web API 2
Web API 2 introduces a new interface IHttpActionResult
(equivalent to ActionResult
in ASP.NET MVC) that greatly simplifies the unit testing story for controllers. A controller method can now return an implementation of IHttpActionResult
, which internally uses the Request
or the UrlHelper
for link generation, so the unit test cares only about the returned IHttpActionResult
instance. The following code shows the equivalent version of the Post
method using an instance of IHttpActionResult
:
public
async
Task
<
IHttpActionResult
>
Post
(
Issue
issue
)
{
var
createdIssue
=
await
_issueSource
.
CreateAsync
(
issue
);
var
result
=
new
CreatedAtRouteNegotiatedContentResult
<
Issue
>(
"DefaultApi"
,
new
Dictionary
<
string
,
object
>
{
{
"id"
,
createdIssue
.
Id
}
},
createdIssue
,
this
);
return
result
;
}
CreatedAtRouteNegotiatedContentResult
is an implementation also included in the framework for handling this scenario. A new resource is created and the location is set in the response message. The unit test is much simpler too, as illustrated in Example 17-13.
[Fact]
public
void
ShouldSetResponseHeadersWhenPOSTForNewIssue
()
{
//Arrange
var
createdIssue
=
new
Issue
();
createdIssue
.
Id
=
"1"
;
_mockIssueSource
.
Setup
(
i
=>
i
.
CreateAsync
(
createdIssue
)).
Returns
(()
=>
Task
.
FromResult
(
createdIssue
));
//Act
var
result
=
_controller
.
Post
(
createdIssue
).
Result
as
CreatedAtRouteNegotiatedContentResult
;
// <1>
//Assert
result
.
ShouldNotBeNull
();
// <2>
result
.
Content
.
ShouldBeType
<
Issue
>();
// <3>
}
The unit test just casts the returned result to the expected type, which is CreateAtRouteNegotiatedContentResult
in this test <1>
, and verifies that the result is not null
<2>
and the content set in that result is an instance of the Issue
type <3>
. No previous initialization code was required, as all the content negotiation and link management logic is now encapsulated in the IHttpActionResult
implementation, which is a concern this unit test does not care about.
Unit Testing a MediaTypeFormatter
As a central piece for handling new media types or content negotiation, a MediaTypeFormatter
implementation involves several aspects you’ll want to address during unit testing. Those aspects include correct handling of the supported media types, converting a model from or to a given media type, or optionally checking the correct configuration of some settings such as encoding or mappings.
We can get a rough idea of what unit testing a MediaTypeFormatter
implementation involves by looking at its class definition in Example 17-14.
public
abstract
class
MediaTypeFormatter
{
public
Collection
<
Encoding
>
SupportedEncodings
{
get
;
}
public
Collection
<
MediaTypeHeaderValue
>
SupportedMediaTypes
{
get
;
}
public
Collection
<
MediaTypeMapping
>
MediaTypeMappings
{
get
;
}
public
abstract
bool
CanReadType
(
Type
type
);
public
abstract
bool
CanWriteType
(
Type
type
);
public
virtual
Task
<
object
>
ReadFromStreamAsync
(
Type
type
,
Stream
readStream
,
HttpContent
content
,
IFormatterLogger
formatterLogger
);
public
virtual
Task
WriteToStreamAsync
(
Type
type
,
object
value
,
Stream
writeStream
,
HttpContent
content
,
TransportContext
transportContext
);
}
The following conditions can be checked with different unit tests:
-
The supported media types (see Example 17-15) were correctly configured in the
SupportedMediaTypes
collection. In the example we built in Chapter 13 for supporting syndication media types such as Atom or RSS, this test would imply that the collection containsapplication/atom+xml
andapplication/rss+xml
for supporting those media types, respectively.
[Fact]
public
void
ShouldSupportAtom
()
{
var
formatter
=
new
SyndicationMediaTypeFormatter
();
Assert
.
True
(
formatter
.
SupportedMediaTypes
.
Any
(
s
=>
s
.
MediaType
==
"application/atom+xml"
));
}
[Fact]
public
void
ShouldSupportRss
()
{
var
formatter
=
new
SyndicationMediaTypeFormatter
();
Assert
.
True
(
formatter
.
SupportedMediaTypes
.
Any
(
s
=>
s
.
MediaType
==
"application/rss+xml"
));
}
-
The implementation supports serializing or deserialization of a given model type in the
CanReadType
andCanWriteType
methods (see Example 17-16).
[Fact]
public
void
ShouldNotReadAnyType
()
{
var
formatter
=
new
SyndicationMediaTypeFormatter
();
var
canRead
=
formatter
.
CanReadType
(
typeof
(
object
));
Assert
.
False
(
canRead
);
}
[Fact]
public
void
ShouldWriteAnyType
()
{
var
formatter
=
new
SyndicationMediaTypeFormatter
();
var
canWrite
=
formatter
.
CanWriteType
(
typeof
(
object
));
Assert
.
True
(
canWrite
);
}
-
The code for writing or reading a model into/out to stream using one of the supported media types is working correctly (see Example 17-17). That implies testing the
WriteToStreamAsync
andReadFromStreamAsync
methods, respectively.
[Fact]
public
void
ShouldSerializeAsAtom
()
{
var
ms
=
new
MemoryStream
();
var
content
=
new
FakeContent
();
content
.
Headers
.
ContentType
=
new
MediaTypeHeaderValue
(
"application/atom+xml"
);
var
formatter
=
new
SyndicationMediaTypeFormatter
();
var
task
=
formatter
.
WriteToStreamAsync
(
typeof
(
List
<
ItemToSerialize
>),
new
List
<
ItemToSerialize
>
{
new
ItemToSerialize
{
ItemName
=
"Test"
}},
ms
,
content
,
new
FakeTransport
()
);
task
.
Wait
();
ms
.
Seek
(
0
,
SeekOrigin
.
Begin
);
var
atomFormatter
=
new
Atom10FeedFormatter
();
atomFormatter
.
ReadFrom
(
XmlReader
.
Create
(
ms
));
Assert
.
Equal
(
1
,
atomFormatter
.
Feed
.
Items
.
Count
());
}
public
class
ItemToSerialize
{
public
string
ItemName
{
get
;
set
;
}
}
public
class
FakeContent
:
HttpContent
{
public
FakeContent
()
:
base
()
{
}
protected
override
Task
SerializeToStreamAsync
(
Stream
stream
,
TransportContext
context
)
{
throw
new
NotImplementedException
();
}
protected
override
bool
TryComputeLength
(
out
long
length
)
{
throw
new
NotImplementedException
();
}
}
public
class
FakeTransport
:
TransportContext
{
public
override
ChannelBinding
GetChannelBinding
(
ChannelBindingKind
kind
)
{
throw
new
NotImplementedException
();
}
}
Example 17-17 shows a unit test that serializes a list of items of the type ItemToSerialize
, also defined in the test as an Atom feed. The test mainly verifies that the SyndicationMediaTypeFormatter
can serialize the list of items when the content type is equal to application/atom+xml
. As the WriteToStreamAsync
method expects instances of HttpContent
and TransportContext
, and those are not used at all in the implementation with the exception of the headers, two fake classes were defined that don’t do anything special. The test also deserializes the stream back to an Atom feed using the WCF syndication class to make sure the serialization was done properly.
-
All the required settings are correctly initialized. For example, if you have a requirement for supporting the media type mappings in the query string, a unit test could check that this mapping was correctly configured in the
MediaTypeMappings
collection, as Example 17-18 demonstrates.
[Fact]
public
void
ShouldMapAtomFormatInQueryString
()
{
var
formatter
=
new
SyndicationMediaTypeFormatter
();
Assert
.
True
(
formatter
.
MediaTypeMappings
.
OfType
<
QueryStringMapping
>()
.
Any
(
m
=>
m
.
QueryStringParameterName
==
"format"
&&
m
.
QueryStringParameterValue
==
"atom"
&&
m
.
MediaType
.
MediaType
==
"application/atom+xml"
));
}
Example 17-18 illustrates a sample of a test that checks if a MediaTypeMapping
was defined as a query string argument for mapping a query string variable format
with value atom
to the media type application/atom+xml
.
Unit Testing an HttpMessageHandler
An HttpMessageHandler
is a generic interception mechanism for the Web API runtime pipeline. It’s asynchronous by nature, and typically contains a single method, Send
Async
, for processing a request message (HttpRequestMessage
) that returns a Task
instance representing some work to obtain a response (HttpResponseMessage
). See Example 17-19.
public
abstract
class
HttpMessageHandler
{
protected
internal
abstract
Task
<
HttpResponseMessage
>
SendAsync
(
HttpRequestMessage
request
,
CancellationToken
cancellationToken
);
}
The SendAsync
method cannot be directly called in a unit test, as it is not public, but the framework provides a class, System.Net.Http.MessageInvoker
, that you can use for that purpose. This class receives the HttpMessageHandler
instance in the constructor and provides a public method, SendAsync
, for invoking the method with the same name on the handler. Example 17-20 simply illustrates how the SendAsync
method in a sample HttpMessageHandler
is unit tested. However, an HttpMessageHandler
might receive external dependencies or contain some other public methods you will want to test as well.
[Fact]
public
void
ShouldInvokeHandler
()
{
var
handler
=
new
SampleHttpMessageHandler
();
var
invoker
=
new
HttpMessageInvoker
(
handler
);
var
task
=
invoker
.
SendAsync
(
new
HttpRequestMessage
(),
new
CancellationToken
());
task
.
Wait
();
var
response
=
task
.
Result
;
// Assertions over the response
// ......
}
Unit Testing an ActionFilterAttribute
Action filters are not any different from the HTTP message handlers when it comes to message interception, but they run much deeper in the runtime pipeline once the action context has been initialized and the action is about to execute. The action filter base class System.Web.Http.Filters.ActionFilterAttribute
(see Example 17-21) provides two methods that can be overridden, OnActionExecuting
and OnActionExecuted
, for intercepting the call before and right after the action has been executed.
public
abstract
class
ActionFilterAttribute
:
FilterAttribute
,
IActionFilter
,
IFilter
{
public
virtual
void
OnActionExecuted
(
HttpActionExecutedContext
actionExecutedContext
);
public
virtual
void
OnActionExecuting
(
HttpActionContext
actionContext
);
}
Both methods are public, so they can be called directly from a unit test. Example 17-22 shows a very basic implementation of a filter for authenticating clients with an application key. The idea is to use this concrete implementation to show how the different scenarios can be unit tested.
public
interface
IKeyVerifier
{
bool
VerifyKey
(
string
key
);
}
public
class
ApplicationKeyActionFilter
:
ActionFilterAttribute
{
public
const
string
KeyHeaderName
=
"X-AuthKey"
;
IKeyVerifier
keyVerifier
;
public
ApplicationKeyActionFilter
()
{
}
public
ApplicationKeyActionFilter
(
IKeyVerifier
keyVerifier
)
// <1>
{
this
.
keyVerifier
=
keyVerifier
;
}
public
Type
KeyVerifierType
// <2>
{
get
;
set
;
}
public
override
void
OnActionExecuting
(
HttpActionContext
actionContext
)
{
if
(
this
.
keyVerifier
==
null
)
{
if
(
this
.
KeyVerifierType
==
null
)
{
throw
new
Exception
(
"The keyVerifierType was not provided"
);
}
this
.
keyVerifier
=
(
IKeyVerifier
)
Activator
.
CreateInstance
(
this
.
KeyVerifierType
);
}
IEnumerable
<
string
>
values
=
null
;
if
(
actionContext
.
Request
.
Headers
.
TryGetValues
(
KeyHeaderName
,
out
values
))
// <3>
{
var
key
=
values
.
First
();
if
(!
this
.
keyVerifier
.
VerifyKey
(
key
))
// <4>
{
actionContext
.
Response
=
new
HttpResponseMessage
(
HttpStatusCode
.
Unauthorized
);
}
}
else
{
actionContext
.
Response
=
new
HttpResponseMessage
(
HttpStatusCode
.
Unauthorized
);
}
base
.
OnActionExecuting
(
actionContext
);
}
}
This action filter receives an instance of IKeyVerifier
, which is used to verify whether a key is valid <1>
. Because an action filter can also be used as an attribute, the implementation provides a property, KeyVerifierType
<2>
, to set the IKeyVerifier
in that scenario. This filter only implements the OnActionExecuting
method, which runs before the action is executed. This implementation checks for an X-Auth
header in the request message set in the context <3>
, and tries to pass the value of that header to the IKeyVerifier
instance for authentication <4>
. If the key cannot be validated or it is not found in the request message, the filter sets a response message with an HTTP status code of 401 Unauthorized
in the current context, and the pipeline execution is interrupted.
The first unit test, shown in Example 17-23, will test the scenario in which a valid key is passed in the request message.
[Fact]
public
class
ApplicationKeyActionFilterFixture
{
public
void
ShouldValidateKey
()
{
var
keyVerifier
=
new
Mock
<
IKeyVerifier
>();
keyVerifier
.
Setup
(
k
=>
k
.
VerifyKey
(
"mykey"
))
.
Returns
(
true
);
// <1>
var
request
=
new
HttpRequestMessage
();
request
.
Headers
.
Add
(
"X-AuthKey"
,
"mykey"
);
// <2>
var
actionContext
=
InitializeActionContext
(
request
);
// <3>
var
filter
=
new
ApplicationKeyActionFilter
(
keyVerifier
.
Object
);
filter
.
OnActionExecuting
(
actionContext
);
// <4>
Assert
.
Null
(
actionContext
.
Response
);
// <5>
}
}
private
HttpActionContext
InitializeActionContext
(
HttpRequestMessage
request
)
{
var
configuration
=
new
HttpConfiguration
();
var
route
=
configuration
.
Routes
.
MapHttpRoute
(
name
:
"DefaultApi"
,
routeTemplate
:
"api/{controller}/{id}"
,
defaults
:
new
{
id
=
RouteParameter
.
Optional
}
);
var
routeData
=
new
HttpRouteData
(
route
,
new
HttpRouteValueDictionary
{
{
"controller"
,
"Issues"
}
}
);
request
.
Properties
[
HttpPropertyKeys
.
HttpRouteDataKey
]
=
routeData
;
var
controllerContext
=
new
HttpControllerContext
(
configuration
,
routeData
,
request
);
var
actionContext
=
new
HttpActionContext
{
ControllerContext
=
controllerContext
};
return
actionContext
;
}
As a first step <1>
, the unit test initializes a mock or fake instance of the IKeyVerifier
that will return true
when the key passed to the VerifyKey
method is equal to mykey
. Secondly <2>
, a new HTTP request message is created and the custom header X-AuthKey
is set to the value expected by the IKeyVerifier
instance. The Action context expected by the filter is initialized in the method InitializeActionContext
<3>
, which requires a lot of common boilerplate code to inject the routing configuration and the request message into the constructor of the HttpControllerContext
class. Finally, the method OnActionExecuting
is invoked <4>
with the initialized context and an assertion is made for a null
response <5>
. If nothing fails in the action filter implementation, the response will never be set in the context, so the test will pass.
Example 17-24 will test the next scenario, in which a key is not valid and a response is returned with the status code 401 (Unauthorized)
.
[Fact]
public
void
ShouldNotValidateKey
()
{
var
keyVerifier
=
new
Mock
<
IKeyVerifier
>();
keyVerifier
.
Setup
(
k
=>
k
.
VerifyKey
(
"mykey"
))
.
Returns
(
true
);
var
request
=
new
HttpRequestMessage
();
request
.
Headers
.
Add
(
ApplicationKeyActionFilter
.
KeyHeaderName
,
"badkey"
);
// <1>
var
actionContext
=
InitializeActionContext
(
request
);
var
filter
=
new
ApplicationKeyActionFilter
(
keyVerifier
.
Object
);
filter
.
OnActionExecuting
(
actionContext
);
Assert
.
NotNull
(
actionContext
.
Response
);
// <2>
Assert
.
Equal
(
HttpStatusCode
.
Unauthorized
,
actionContext
.
Response
.
StatusCode
);
// <3>
}
The main difference with the previous test is that the application set in the request message <1>
is different from the one expected by the IKeyVerifier
mock instance. After the OnActionExecuting
method is invoked, two assertions are made to make sure the response set in the context is not null
<2>
and its status code is equal to 401 (Unauthorized)
<3>
.
Unit Testing Routes
Route configuration is another aspect that you might want to cover with unit testing. Although it’s not a component itself, a complex route configuration that does not follow the common conventions might lead to some problems that you will want to figure out sooner rather than later, and far before the implementation is deployed.
The bad news is that ASP.NET Web API does not offer any support for unit testing routes out-of-the-box, so custom code is required. That custom code will basically use some of the built-in Web API infrastructure components, like the DefaultHttpControllerSelector
and ApiControllerActionSelector
, to infer the controller type and action name for a given HttpRequestMessage
and routing configuration. See Example 17-25.
public
static
class
RouteTester
{
public
static
void
TestRoutes
(
HttpConfiguration
configuration
,
HttpRequestMessage
request
,
Action
<
Type
,
string
>
callback
)
{
var
routeData
=
configuration
.
Routes
.
GetRouteData
(
request
);
request
.
Properties
[
HttpPropertyKeys
.
HttpRouteDataKey
]
=
routeData
;
var
controllerSelector
=
new
DefaultHttpControllerSelector
(
configuration
);
// <1>
var
controllerContext
=
new
HttpControllerContext
(
configuration
,
routeData
,
request
);
controllerContext
.
ControllerDescriptor
=
controllerSelector
.
SelectController
(
request
);
// <2>
var
actionSelector
=
new
ApiControllerActionSelector
();
// <3>
var
action
=
actionSelector
.
SelectAction
(
controllerContext
).
ActionName
;
// <4>
var
controllerType
=
controllerContext
.
ControllerDescriptor
.
ControllerType
;
// <5>
callback
(
controllerType
,
action
);
// <5>
}
}
Example 17-25 illustrates a generic method that receives an instance of HttpConfiguration
with the routing configuration and an HttpRequestMessage
, and invokes a callback with the selected controller type and action name. This method first instantiates a DefaultHttpControllerSelector
class using the HttpConfiguration
received as an argument to determine the controller type <2>
. The controller is selected afterward with the HttpRequestMessage
also passed as an argument <3>
. Once the controller is selected, an ApiControllerActionSelector
is instantiated next to infer the action name <4>
. The action name and controller type are obtained in <5>
and <6>
. Finally, a callback is called with the inferred controller type and action name. This callback will be used by the unit test to perform the assertions. See Example 17-26.
[Fact]
public
void
ShouldRouteToIssueGET
()
{
var
config
=
new
HttpConfiguration
();
config
.
Routes
.
MapHttpRoute
(
name
:
"Default"
,
routeTemplate
:
"api/{controller}/{id}"
);
// <1>
var
request
=
new
HttpRequestMessage
(
HttpMethod
.
Get
,
"http://www.example.com/api/Issues/1"
);
// <2>
RouteTester
.
TestRoutes
(
config
,
request
,
// <3>
(
controllerType
,
action
)
=>
{
Assert
.
Equal
(
typeof
(
IssuesController
),
controllerType
);
Assert
.
Equal
(
"Get"
,
action
);
});
}
Example 17-26 illustrates how the RouteTester
class can be used in a unit test to verify a route configuration. An HttpConfiguration
is initialized and configured with the routes to test <1>
, and also an HttpRequestMessage
with the HTTP verb and URL to invoke <2>
. As the final step, the RouteTester
is used with the configuration and request instances to determine the controller type and action name. As part of the callback, the test defines the assertions for comparing the inferred controller type and action name with the expected ones <3>
.
Integration Tests in ASP.NET Web API
Thus far we have discussed unit testing, which focuses on testing components in isolation, but what happens if you would like to test how all your components collaborate in a given scenario? This is where you will find integration testing very useful. In the case of a Web API, integration testing focuses more on testing a complete call end to end from the client to the service, including all the components in the stack such as controllers, filters, message handlers, or any other component configured in your Web API runtime. For example, you might want to use an integration test to enable basic authentication with an HttpMessageHandler
and verify how that handler behaves with your existing controllers from the point of a view of a client application. Ideally, you will also unit test those components to make sure they behave correctly in isolation.
For doing integration testing in ASP.NET Web API, we will use HttpClient
, which can handle requests in an in-memory hosted server. This has some evident advantages for simplifying the tests, as there is no need to open ports or send messages across the network. As shown in Example 17-27, the HttpClient
class contains several constructors that receive an HttpMessageHandler
instance. As covered in Chapter 4, the HttpServer
class is an HttpMessageHandler
implementation, which means it can be directly injected in an HttpClient
instance to automatically handle any message sent by the client in a test.
public
class
HttpClient
:
HttpMessageInvoker
{
public
HttpClient
();
public
HttpClient
(
HttpMessageHandler
handler
);
public
HttpClient
(
HttpMessageHandler
handler
,
bool
disposeHandler
);
}
We can configure an HttpServer
instance with the HttpMessageHandler
from our previous implementation and use it with an HttpClient
instance within an integration test to verify how the scenario works end to end. See Example 17-28.
public
class
BasicAuthenticationIntegrationTests
{
[Fact]
public
ShouldReturn404IfCredentialsNotSpecified
()
{
var
config
=
new
HttpConfiguration
();
config
.
Routes
.
MapHttpRoute
(
name
:
"Default"
,
routeTemplate
:
"api/{controller}/{action}/{id}"
,
defaults
:
new
{
id
=
RouteParameter
.
Optional
});
// <1>
config
.
MessageHandlers
.
Add
(
new
BasicAuthHttpMessageHandler
());
// <2>
var
server
=
new
HttpServer
(
config
);
var
client
=
new
HttpClient
(
server
);
// <3>
var
task
=
client
.
GetAsync
(
"http://test.com/issues"
);
// <4>
task
.
Wait
();
var
response
=
task
.
Result
;
Assert
.
AreEqual
(
HttpStatusCode
.
Unauthorized
,
response
.
StatusCode
);
// <5>
}
}
As shown in Example 17-28, we can still use a unit testing framework for automating the integration tests. Our test is configuring a server in-memory with a default route <1>
and a BasicAuthHttpMessageHandler
<2>
, which internally implements basic authentication. That server is injected in the HttpClient
<3>
, so the call to http://test.com/issues with GetAsync
will be routed to that server <4>
. In the case of this test, no authorization header was set in the HttpClient
, so the expected behavior is that the BasicAuthHttp
Handler
returns a response message with status code 404 (Unauthorized)
<5>
. Authentication is just a scenario where integration testing makes sense, but as you can imagine, this idea can be extended to any scenario that requires coordination between multiple components.
Conclusion
TDD can be used as a very effective tool to drive the design and implementation of your Web API. As a side effect, you will get unit tests reflecting the expected behavior of the implementation that you can also use to make progressive enhancements in the existing code. Those tests can be used to make sure that nothing broke with the introduction of new changes and that the implementation still satisfies the expected behavior. There are two commonly used practices with TDD: dependency injection and code refactoring. While the former focuses on generating more testable code by removing explicit dependencies, the latter is used to improve the quality of the existing code. In addition to unit testing, which focuses on testing specific pieces of code in isolation, you can also use integration testing for testing a scenario end to end, and see how the different components in the implementation interact.
Get Designing Evolvable Web APIs with ASP.NET now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.