This post continues onUnit testing and code coverage for ASP.NET Web API (1/2).
Much about the topic isinspired from the truly magnificent book "The Clean Coder: A Code of Conduct for Professional Programmers" of Robert C. Martin Series, which of course, I definitely recommend.
Specifying the low level architecture
Professional software developers always test their code. It is part of our daily job, we should be proud and flexible on writing tests. It isa proof that our code actually follows our intent, at least on system'slow level. There are many more tests to be followed, composing a testing strategy, but this post isgoing to focus solely onone aspect of such strategy, the unit tests.
TLDR; Code can be foundhere.
The three lawsof TDD
Robert C. Martin, aka Uncle Bob, has defined some three simple rules that describeTest Driven Development.
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test that is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more productioncode than is sufficient to pass the one failing unit test.
Rule #1 says that you have to write your test first, rather than jumping directly to your business code.
Rule #2 is more peculiar as it states that you should not write more of a unit test if that unit test fails. You need to stop, switch to production code and make it pass.
Rule #3 states that you shouldonly write enough production code to make the test compile or pass. No more.
These three laws lock you into a cycle that is, perhaps, thirty seconds long. You switch from unit test to production code and vice versa. Adding a bit to the test code, adding a bit to the production code. This cycle, for some people may seem slow.Some, have the feeling that they mightwrite more is they don't write tests at all. But this is an illusion, we tend to write bad code without tests and we spend a lot of time in manual tests. This is the recipefor disaster. We (and probably other people) are not happy with ourselves, in the end.
Search for TDD kata's over the internet, see other people's views and comparisons onworking on specific problems with or without TDD. Then try to work on kata's by yourself, using each approach. Try to ride the beast andyou will be amazed by the end result.
Going green
In order to create some unit tests you need to download the following nuget packages:
- NUnit3. A unit testing framework. Provides all tools needed to write tests, set them up, tear them down, etc.
- Moq. A mocking framework.Provides necessary API to mock interface or abstract dependencies into modules.
Anatomy
A unit testcan be a class decorated by the TestFixtureAttribute
. Your test methods must be decorated bythe TestAttribute
in order to be identified as tests. If you need to setup some code before running a test, you should create a method with is decorated by the SetUpAttribute
and there initialize all yourcode needed for the underlying tests.
[TestFixture]
public class DemoTests
{
[Test]
public void UnitOfWork_StateUnderTest_ExpectedBehavior_Test()
{
// Arrange
// Initialization code here...
// Act
// Actual calls...
// Assert
// Assertions here...
}
}
Test code above follows naming standards. It also follows the AAA (Arrange, Act, Assert) pattern. It is a patternfor writing clean tests, with parts of code related to each other grouped together. What is that it makes a test clean though? Readability is the answer. We want the tests to be readable because their intent is not only to test code but to document it as well, which means other individuals will have to go through them.
Initialization code is grouped together in an imaginary Arrange group. Same for Actand Assert group.Those groups are labeled by one line comments. If you want, you can omit those comments, and separate the groupswith a newline. Point is toseparate conceptual affinity groups in order to maximize test's readability.
I have written sometests for this application, some of them will be shown here, some won't make it for brevity's sake. I would suggest to go to the repository and check yourselves to get the whole picture.
There are tests forPeople.Domain project, which consist of ApplicationDbContextTests and RepositoryTests.
There are tests for People.Services project,which consist of PersonServiceTests.
There are tests for People.SelfHostedApi project, which consist of tests for controllers, action selection, routing, custom authorization server.
Let'sexplore some various code samples from them.
Testing controllers
In People.SelfHostedApi.Tests project,under the Controllers directory you can find tests for Web API controllers.
Let's see PersonController unit tests. This controller receives an IPersonService type, through constructor injection. So, in tests I need to make sure I pass a mocked object of this type, controlling calls to this object.
Setup dependencies
private Mock<IPersonService> _personService;
[SetUp]
public void Setup()
{
_personService = new Mock<IPersonService>();
}
This creates a mock of the IPersonService type. The _personService
field, will be fed into PeopleController's constructor. See the snippet bellow:
[Test]
public void PeopleController_Get_PeopleFromService_ReturnsOkResultWithPeopleListAsContent_Test()
{
// Arrange
var people = CreatePeopleList();
_personService.Setup(m => m.GetPeople()).Returns(people.AsQueryable());
var controller = new PeopleController(_personService.Object);
// Act
var result = controller.Get() as OkNegotiatedContentResult<List<Person>>;
// Assert
IsNotNull(result);
AreEqual(people.Count, result.Content.Count);
}
The _personService
mock object exposes the mocked object instance by accessing the itsObject
property. Before I inject it into the controller, I fist made sure to mock the GetPeople()
method of the service, as this is the one I care about in this scope. This method will return a mockedresult, a list of Person objects.
In act section, I call the actual method I want to test.
Inassert section, I justmake sure that code does what I expect it to do.
IHttpActionResult
In the act section, from previous example, theGet action returns an IHttpActionResult
. In order to get the command along with the contents it returns we need to use the correct typefrom System.Web.Http.Results
namespace. Web API teammade this feature available in Web API 2, in order to make unit testing easy.
For OK results with content, we expect OkNegotiatedContentResult<T>
. T
represents the type of content that is returned. Beware through, if you put the wrong T
type intest, you are going to get a null value. For example, if you return a collection, don't put OkNegotiatedContentResult<IEnumerable<Person>>
as thisgoing to be null. If you return a List<T>
as a content, put a List<T>
in the type. Same goes for other types like CreatedNegotiatedContentResult<T>.
Now, for OK results without content (empty) use the OkResult
type to cast. Same goes for other types that return empty content, like BadRequestResult
, NotFoundResult
, etc.
This is atestto verify that a NotFoundResult is returned to the client, when person with specific id is not found:
[Test]
public void PeopleController_Get_PersonByIdFromService_PersonCouldNotBeFound_ReturnsNotFoundResult_Test()
{
// Arrange
const int id = 1;
_personService.Setup(m => m.GetPerson(It.Is<int>(s => s == id))).Returns(() => null);
var controller = new PeopleController(_personService.Object);
// Act
var result = controller.Get(id) as NotFoundResult;
// Assert
IsNotNull(result);
}
In case ofBadRequestResult
you cast it to this type when you return an empty BadRequest without content, like the following
if (person == null)
return BadRequest();
In case you return a BadRequest with content, you cast a BadRequestErrorMessageResult
if (id < 0)
return BadRequest($"{nameof(id)} is not valid.");
And the corresponding test case:
[Test]
public void PeopleController_Delete_PersonIdIsNotValid_ReturnsBadRequest_Test()
{
// Arrange
var controller = new PeopleController(_personService.Object);
// Act
var result = controller.Delete(-5) as BadRequestErrorMessageResult;
// Assert
IsNotNull(result);
AreEqual("id is not valid.", result.Message);
}
Testing actions which do not dependon controller context
All tests demonstrated above, testcontroller actions which do not depend on controller context at all. This means,there is no need to create an HttpConfiguration
, or HttpControllerContext
, or HttpRequestMessage
or anything like that, just call the controller's action as normal through code and test the behavior/result returned.
Testing actions which depend on controller context
These tests are similar to previous ones, but with one difference. You are responsible to setup a context for the controller under test. Youactually need that in order to get the test passing, or else you are going to receive some NullReferenceException
, ArgumentNullException
exceptions.
So, let's see this method from PersonController, which creates a new Person, returns a 202 Created response code, alongwith the resource URI.
public IHttpActionResult Post(Person person)
{
if (person == null)
return BadRequest();
_service.Create(person);
return Created($"{Request.RequestUri.AbsoluteUri}/{person.Id}", person);
}
Have you noticed the problem yet? The Created method receives a URI in string and a Person object. The problem is that thecreation of the URI depends on the Request
object of the controller, which happens to be ofHttpRequestMessage
type. So, it's required to create a new instance of the PersonController and pass an HttpRequestMessage
instance to its Request
property.
The following test does exactly this:
[Test]
public void PeopleController_Post_PersonToService_ReturnsCreatedResult_Test()
{
// Arrange
var person = CreateDefaultPersonObject();
var request = new HttpRequestMessage
{
RequestUri = new System.Uri(BaseUri)
};
var controller = new PeopleController(_personService.Object)
{
Request = request
};
// Act
var result = controller.Post(person) as CreatedNegotiatedContentResult<Person>;
// Assert
IsNotNull(result);
AreEqual($"{BaseUri}/1", result.Location.ToString());
AreEqual(Id, result.Content.Id);
AreEqual(Name, result.Content.Name);
AreEqual(Age, result.Content.Age);
}
In the assertion conceptual affinity block, except of asserting the result content, there is assertion on the current location the new resource is created at. Thelocation consists of the absolute URI of the request plus the Id of thePerson object created.
Testing routes
Routing in ASP.NET Web API is based on the IHttpRoute
interface. If you open the source code of the IHttpRoute interface, you will see various properties and methods, but the main point of interest here is the GetRouteData
method.By this, it is easy to break down the URL to its components and parameters, which is exactly what the following test is doing. Currently, there is only one route configured, which is the Default route, a pretty standard one to match a controller by its name, with an optional Id parameter.
As a side note, the following test is a data driven test, meaning test can be divided tomore than one test cases, whereas each runs individually, passing input to the actual test. This can be achieved using the TestCaseAttribute
of NUnit.
[Test]
[TestCase("http://localhost:3001/invalid/route", "GET", false, null, null)]
[TestCase("http://localhost:3001/api/people/", "GET", true, "people", null)]
[TestCase("http://localhost:3001/api/people/", "POST", true, "people", null)]
[TestCase("http://localhost:3001/api/people/1", "PUT", true, "people", "1")]
[TestCase("http://localhost:3001/api/people/1", "GET", true, "people", "1")]
[TestCase("http://localhost:3001/api/people/1", "DELETE", true, "people", "1")]
[TestCase("http://localhost:3001/api/user/", "GET", true, "user", null)]
public void DefaultRoute_Returns_Correct_RouteData_Test(string url, string method, bool exists, string controller, string id)
{
// Arrange
var config = SetupHttpConfiguration();
var request = SetupHttpRequestMessage(url, method);
// Act
var routeData = config.Routes.GetRouteData(request);
// Assert
AreEqual(exists, routeData != null);
if (exists)
{
AreEqual(controller, routeData.Values["controller"]);
AreEqual(id ?? (object)RouteParameter.Optional, routeData.Values["id"]);
}
}
Lots of cases here. Each of the parameters on the TestCaseAttribute are passed as actual parameters to the Test. You need to create anHttpConfiguration object, which registers the routes (in this case the default route), as well as to create an HttpRequestMessage with the appropriate method and URL.
In HttpConfiguration initialization, make sure to call the EnsureInitialized
method on the configuration object, after registering the required routes.These actions take place in two following public utility methods. Reason? Readability, as the test above seems much cleaner without having all these methods internals. All you need is some expressive names.
public static HttpConfiguration SetupHttpConfiguration()
{
var config = new HttpConfiguration();
WebApiRouteConfig.Register(config);
config.EnsureInitialized();
return config;
}
public static HttpRequestMessage SetupHttpRequestMessage(string url, string method)
{
var httpMethod = new HttpMethod(method);
return new HttpRequestMessage(httpMethod, url);
}
Testing controller and action selection
Last bit to cover in thisrather lengthy blog, is about controller and action selection in Web API. You need to make sure that the correct controller and correct action is selected on each request. You can test the routes, but you won't be sure which controller and action is selected. Methods in Web API, when the design is Restful, are selected based onthe request URI and HTTP method, which differentiates from classic RPC calls to specificmethods.
There is a way to test this functionality as well, due to IHttpControllerSelector
and IHttpActionSelector
interfaces, which are used by the framework to find the appropriate controller and action.
There are two ways to write those tests, one is to rely on the routing result to be correct, hence takes a form of integration test, while the other is to provide your own route data to decouple the test from routing. First option is much simpler, with little setup, and is the onechosen to test this behavior.
Look at the following example:
[Test]
[TestCase("http://localhost:3001/api/people/", "GET", typeof(PeopleController), "Get")]
[TestCase("http://localhost:3001/api/people/1", "GET", typeof(PeopleController), "Get")]
[TestCase("http://localhost:3001/api/people/1", "PUT", typeof(PeopleController), "Put")]
[TestCase("http://localhost:3001/api/people/1", "DELETE", typeof(PeopleController), "Delete")]
[TestCase("http://localhost:3001/api/people/", "POST", typeof(PeopleController), "Post")]
public void CorrectControllerAndActionAreSelected_Test(string url, string method, Type controller, string action)
{
// Arrange
var config = SetupHttpConfiguration();
var actionSelector = config.Services.GetActionSelector();
var controllerSelector = config.Services.GetHttpControllerSelector();
var request = SetupHttpRequestMessageRequest(url, method);
var routeData = SetupRouteData(request, config);
SetupRequestProperties(request, routeData, config);
// Act
var controllerDescriptor = controllerSelector.SelectController(request);
var context = SetupHttpControllerContext(config, routeData, request, controllerDescriptor);
var actionDescriptor = actionSelector.SelectAction(context);
// Assert
AreEqual(controller, controllerDescriptor.ControllerType);
AreEqual(action, actionDescriptor.ActionName);
}
Look at the URI's tested. Yousee that some URI's are similar for different actions, with only difference being the HTTPmethod.This test makes sure that theappropriate action is picked up on a request.
Also, noticehow IHttpActionSelectorand IHttpControllerSelector instances are fetched, through the HttpConfiguration.Services
object, which is an IoC container, implementing the Service Locator pattern, of type ServicesContainer
. This class is designed to resolve dependencies of the framework itself, so it's easy for us to fetch the actionand controller selector instances.
Then,aControllerDescriptor
is fetched from SelectController
method, of course by relying on the request. In order to get theActionDescriptor
, an HttpControllerContext
is required (see the utility methods below).
Finally, the assertion is performed on the controller type and the action name to ensure the correct controller and action of that controller are selected.
The utilitymethods forthe test aren't something special, one creates a new HttpControllerContext
, while the other an HttpConfiguration
as shown earlier.
private static HttpControllerContext SetupHttpControllerContext(HttpConfiguration config, IHttpRouteData routeData,
HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor)
{
var context = new HttpControllerContext(config, routeData, request)
{
ControllerDescriptor = controllerDescriptor
};
return context;
}
Code coverage
In order toget some reports on code coverage you need to use the following nuget packages:
- OpenCover (currently using the 4.6.519 version)
- ReportGenerator (currently using the 2.4.5 version)
- NUnitConsoleRunner (currently using 3.4.1 version)
Now, we want to have report for each test project in the solution, while the solution holds 3 different test projects.
Steps are the following:
- Create a .bat file in your project root.
- On the file go to properties (Alt+Enter) and selectCopy if newer for the Copy to Output directory option.
- Paste the following code
"..\..\..\packages\OpenCover.4.6.519\tools\OpenCover.Console.exe" -target:"..\..\..\packages\NUnit.ConsoleRunner.3.4.1\tools\nunit3-console.exe" -targetargs:"NAME_OF_TEST_PROJECT.dll" -filter:"+[NAME_OF_PROJECT]NAME_OF_PROJECT*" -excludebyattribute:"System.CodeDom.Compiler.GeneratedCodeAttribute" -register:user -output:"_CodeCoverageResult.xml"
@pause
"..\..\..\packages\ReportGenerator.2.4.5.0\tools\ReportGenerator.exe" "-reports:_CodeCoverageResult.xml" "-targetdir:_CodeCoverageReport";
@pause
:RunLaunchReport
start "report" "_CodeCoverageReport\index.htm";
exit /b %errorlevel%
Code above does three things.
- Uses OpenCover tool, in conjuction with NUnit3 to create a coverage result in an XML format.
- Uses the ReportGenerator tool, to get the coverage result, which is an XML file and convert human readable, pleasing HTML reports.
- Launches browser to show the report.
Take extra care on the OpenCover, NunitConsoleRunner and ReportGenerator version numbers. Be sure to use the correct ones. Go to your packages folder in root and double check.
Replace theNAME_OF_TEST_PROJECT with your test project name (my test project for example is People.Services.Tests) and on filter, replace the NAME_OF_PROJECT with the project name that you are currently testing (I am testing the People.Services project in People.Services.Tests).
Rest of the code is pretty standard, the ReportGenerator is running on the same directory as OpenCover created the _CodeCoverageResult.xml file. It picks it up, creates a new directory _CodeCoverageReport and places all the HTML reports there.
Last three lines of code, are actuallystarting the default browser, to open the ReportGenerator's HTML report.
Make sure to run the .bat file from your project's /bin/CONF/ directory (CONF might be your Debug or Release configuration, whatever you are building against).
A little bit aboutfilters
You need to add filters in your OpenCover configuration, you need to instruct the tool what to look for and what to exclude, or else your reports will be skewed. You use the -filter
switch.
-filter: "+[People.Services]People.Services*"
. This adds a specific namespace to the coverage. It adds alltypes in People.Services assembly, that start with the People.Services namespace. If you want to exclude something use the - symbol istead.
-filter:"+[People.Domain]People.Domain* -[*]People.Domain.Migrations* -[*]People.Domain.Context.BaseDbContext* -[*]People.Domain.Entities*"
. This filter includes all types under People.Domain namespace in People.Domain assembly, but excludes every type that is under People.Domain.Migrations
, People.Domain.Context.BaseDbContext
, People.Domain.Entities
namespaces.
For more information look at the project's wiki, understanding filters.
Listing 1-1. People.Domain.Tests coverage report. 100%
Listing 1-2. People.Services.Tests coverage report. 100%
Listing 1-3. People.SelfHostedApi.Tests coverage report. 90.5%
Some code slips away from the coverage and is seen as untested, but it's code thatdoesn't have a value to be tested, like theframework's code. Possibledefects from such code can be easily tracked inupper level tests, like acceptance, integration, system or exploratory tests.
Summary
In this post, welearnt how to author unit tests forthe class libraries, representing different application layers, as well as authoring unit tests for ASP.NET Web API. We also testedWeb API specific components, like routes, action selection, introducing NUnit's data driven tests.
Finally, we learnt how toinstall and configure OpenCover and ReportGenerator tools, make them work together with the tests and code, in order to provide code coverage reports.
In the next session I am going to talk a little bit more about BDD and integration testing in ASP.NET Web API.I am also going to explore testing in OWIN pipeline.
Comments powered by Disqus.