Software maintenance is the inevitable part of the development process and one that could give developers the most trouble. We’ve all been there, whether we left our code in someone else’s care, or we’ve inherited some legacy code.
This doesn’t necessarily need to be a bad thing and there are ways to improve our code and make it more maintainable. Unit testing plays a very important role in making software more maintainable.
Our intention in this post is to make an intro to unit testing of ASP.NET Core Web API application.
You can download the source code from the repo on GitHub.
This project is created in .NET Core v2.2, so if you have trouble starting it, please download and install SDK for version 2.2.
In this article, we are going to talk about different segments of unit testing:
- About Unit Testing in General
- Preparing the Example Project
- ShoppingCartService Explanation
- Creating a Testing Project
- Let’s write some unit tests!
- Testing Our Actions
- Summary
- Conclusion
About Unit Testing in General
What is unit testing in the first place? It begins by defining what a „unit“ is and although this is not strictly defined, unit represents a unit of work – usually a single method in our code.
We test these units individually, making sure that each of them is doing exactly that what it is written for.
Nothing more, nothing less.
What is important to understand is that we are not testing the behavior of the dependencies of that method. That is what the integration tests are for.
Preparing the Example Project
We will use Visual Studio 2017 to create our example project and it will be ASP.NET Core Web API application. Let’s start by creating a new ASP.NET Core Web Application:
After choosing the name for or solution and the project, we get to chose which type of web application we want to create. In our case that would be the API project:
When we create ASP.NET Core API application initially, it comes with an example ValuesController
class.
Let’s delete that one and create our own example controller named ShoppingCartController
. Here we can define CRUD operations you would typically have on an entity based controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
[Route("api/[controller]")] [ApiController] public class ShoppingCartController : ControllerBase { private readonly IShoppingCartService _service; public ShoppingCartController(IShoppingCartService service) { _service = service; } // GET api/shoppingcart [HttpGet] public ActionResult<IEnumerable<ShoppingItem>> Get() { var items = _service.GetAllItems(); return Ok(items); } // GET api/shoppingcart/5 [HttpGet("{id}")] public ActionResult<ShoppingItem> Get(Guid id) { var item = _service.GetById(id); if (item == null) { return NotFound(); } return Ok(item); } // POST api/shoppingcart [HttpPost] public ActionResult Post([FromBody] ShoppingItem value) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var item = _service.Add(value); return CreatedAtAction("Get", new { id = item.Id }, item); } // DELETE api/shoppingcart/5 [HttpDelete("{id}")] public ActionResult Remove(Guid id) { var existingItem = _service.GetById(id); if (existingItem == null) { return NotFound(); } _service.Remove(id); return Ok(); } } |
Nothing special about the code here, we’ve got a simple example of a shopping cart controller where we have methods to get, add and remove items from the cart.
ShoppingCartService Explanation
To access the data source, we are using ShoppingService
class which implements IShoppingService
interface. This allows us to follow the dependency injection principle, which is used heavily for purpose of for the unit testing.
Using dependency injection, we can inject whatever implementation of IShoppingCart
interface we want into our test class.
Please note that methods of the service are not implemented in the example project, because we are not focusing on the service implementation here, testing the controller is the main goal. In the real project, you would probably use some data access logic in your service methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class ShoppingCartService : IShoppingCartService { public ShoppingItem Add(ShoppingItem newItem) { throw new NotImplementedException(); } public IEnumerable<ShoppingItem> GetAllItems() { throw new NotImplementedException(); } public ShoppingItem GetById(Guid id) { throw new NotImplementedException(); } public void Remove(Guid id) { throw new NotImplementedException(); } } |
IShoppingService
contains signatures of all the methods seen in the ShoppingCartService
:
1 2 3 4 5 6 7 |
public interface IShoppingCartService { IEnumerable<ShoppingItem> GetAllItems(); ShoppingItem Add(ShoppingItem newItem); ShoppingItem GetById(Guid id); void Remove(Guid id); } |
ShoppingItem
is our main (and only 🙂 ) entity with just a few fields:
1 2 3 4 5 6 7 8 |
public class ShoppingItem { public Guid Id { get; set; } [Required] public string Name { get; set; } public decimal Price { get; set; } public string Manufacturer { get; set; } } |
As we are using dependency injection to create instances of our services, make sure not to forget to register the service in the Startup
class:
1 |
services.AddScoped<IShoppingCartService, ShoppingCartService>(); |
Creating a Testing Project
Finally, we come to the point when we need to create a new project where our tests are going to be. Conveniently for us, there is a xUnit
testing project template out-of-the-box when using visual studio 2017, so we are going to make use of that.
The xUnit
is an open source unit testing tool for the .NET framework that simplifies the testing process and allows us to spend more time focusing on writing our tests:
Now we have a new project in our solution named web-api-tests
. Next thing we should do is to add the reference to the project we are about to write tests for:
Also, we need to make sure that we have a reference to ASP.NET Core package because we will need that in order to write our tests. You can add this reference manually by editing the project file, but the more convenient way to do it is by using the NuGet package manager:
At this time we should create our fake implementation of the IShoppingCartService
interface, which we are going to inject to our controller at the time of testing.
It has an in-memory collection which we are going to fill up with our dummy data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class ShoppingCartServiceFake: IShoppingCartService { private readonly List<ShoppingItem> _shoppingCart; public ShoppingCartServiceFake() { _shoppingCart = new List<ShoppingItem>() { new ShoppingItem() { Id = new Guid("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200"), Name = "Orange Juice", Manufacturer="Orange Tree", Price = 5.00M }, new ShoppingItem() { Id = new Guid("815accac-fd5b-478a-a9d6-f171a2f6ae7f"), Name = "Diary Milk", Manufacturer="Cow", Price = 4.00M }, new ShoppingItem() { Id = new Guid("33704c4a-5b87-464c-bfb6-51971b4d18ad"), Name = "Frozen Pizza", Manufacturer="Uncle Mickey", Price = 12.00M } }; } public IEnumerable<ShoppingItem> GetAllItems() { return _shoppingCart; } public ShoppingItem Add(ShoppingItem newItem) { newItem.Id = Guid.NewGuid(); _shoppingCart.Add(newItem); return newItem; } public ShoppingItem GetById(Guid id) { return _shoppingCart.Where(a => a.Id == id) .FirstOrDefault(); } public void Remove(Guid id) { var existing = _shoppingCart.First(a => a.Id == id); _shoppingCart.Remove(existing); } } |
Instead of creating fake service manually, we could’ve used one of the many mocking frameworks available. One of those frameworks is called Moq. You can get more information about it on the official GitHub page: https://github.com/Moq/moq4/wiki/Quickstart.
Let’s write some unit tests!
Now we are all set and ready to write tests for our first unit of work – the Get
method in our ShoppingCartController
.
We will decorate test methods with the [Fact]
attribute, which is used by the xUnit
framework, marking them as the actual testing methods. Besides the tests methods, we can have any number of helper methods in the test class as well.
When writing unit tests it is usually the practice to follow the AAA principle (Arrange, Act and Assert):
Arrange – this is where you would typically prepare everything for the test, in other words, prepare the scene for testing (creating the objects and setting them up as necessary)
Act – this is where the method we are testing is executed
Assert – this is the final part of the test where we compare what we expect to happen with the actual result of the test method execution
Test method names should be as descriptive as possible. In most of the cases, it is possible to name the method so that it is not even necessary to read the actual code to understand what is being tested.
In the example we use the naming convention in which the first part represents the name of the method being tested, the second part tells us more about the testing scenario and last part is the expected result.
Generally, the logic inside our controllers should be minimal and not so focused on business logic or infrastructure (ec. data access). We want to test the controller logic and not the frameworks we are using.
We need to test how the controller behaves based on the validity of the inputs and controller responses based on the result of the operation it performs.
Testing Our Actions
The first method we are testing is the Get
method and there we will want to verify the following:
- Whether the method returns the
OkObjectResult
which represents 200 HTTP code response - Whether returned object contains our list of
ShoppingItems
and all of our items
Testing the Get Method
So let’s see how we go about testing our method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class ShoppingCartControllerTest { ShoppingCartController _controller; IShoppingCartService _service; public ShoppingCartControllerTest() { _service = new ShoppingCartServiceFake(); _controller = new ShoppingCartController(_service); } [Fact] public void Get_WhenCalled_ReturnsOkResult() { // Act var okResult = _controller.Get(); // Assert Assert.IsType<OkObjectResult>(okResult.Result); } [Fact] public void Get_WhenCalled_ReturnsAllItems() { // Act var okResult = _controller.Get().Result as OkObjectResult; // Assert var items = Assert.IsType<List<ShoppingItem>>(okResult.Value); Assert.Equal(3, items.Count); } } |
We create an instance of the ShoppingCartController
object in the test class and that is the class we want to test. It is important to note here that this constructor is called before each test method, meaning that we are always resetting the controller state and performing the test on the fresh object.
This is important because the test methods should not be dependant on one another and we should get the same testing results, no matter how many times we run the tests and in which order we run them.
Testing the GetById method
Now let’s see how we can test the GetById
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
[Fact] public void GetById_UnknownGuidPassed_ReturnsNotFoundResult() { // Act var notFoundResult = _controller.Get(Guid.NewGuid()); // Assert Assert.IsType<NotFoundResult>(notFoundResult.Result); } [Fact] public void GetById_ExistingGuidPassed_ReturnsOkResult() { // Arrange var testGuid = new Guid("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200"); // Act var okResult = _controller.Get(testGuid); // Assert Assert.IsType<OkObjectResult>(okResult.Result); } [Fact] public void GetById_ExistingGuidPassed_ReturnsRightItem() { // Arrange var testGuid = new Guid("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200"); // Act var okResult = _controller.Get(testGuid).Result as OkObjectResult; // Assert Assert.IsType<ShoppingItem>(okResult.Value); Assert.Equal(testGuid, (okResult.Value as ShoppingItem).Id); } |
Firstly we verify that the controller will return 404 status code (Not Found) if someone asks for the non-existing ShoppingItem
. Secondly, we test if 200 code is returned when existing object is asked for and lastly we check if the right object is returned.
Testing the Add Method
Let’s see how we can deal with the Add method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
[Fact] public void Add_InvalidObjectPassed_ReturnsBadRequest() { // Arrange var nameMissingItem = new ShoppingItem() { Manufacturer = "Guinness", Price = 12.00M }; _controller.ModelState.AddModelError("Name", "Required"); // Act var badResponse = _controller.Post(nameMissingItem); // Assert Assert.IsType<BadRequestObjectResult>(badResponse); } [Fact] public void Add_ValidObjectPassed_ReturnsCreatedResponse() { // Arrange ShoppingItem testItem = new ShoppingItem() { Name = "Guinness Original 6 Pack", Manufacturer = "Guinness", Price = 12.00M }; // Act var createdResponse = _controller.Post(testItem); // Assert Assert.IsType<CreatedAtActionResult>(createdResponse); } [Fact] public void Add_ValidObjectPassed_ReturnedResponseHasCreatedItem() { // Arrange var testItem = new ShoppingItem() { Name = "Guinness Original 6 Pack", Manufacturer = "Guinness", Price = 12.00M }; // Act var createdResponse = _controller.Post(testItem) as CreatedAtActionResult; var item = createdResponse.Value as ShoppingItem; // Assert Assert.IsType<ShoppingItem>(item); Assert.Equal("Guinness Original 6 Pack", item.Name); } |
Once again we are testing that the right objects are returned when someone calls the method, but there is something important to note here.
Among other things, we are testing if ModelState
is validated and the proper response is returned in the case that model is not valid. But to achieve this, it is not enough to just pass the invalid object to the Add method. That wouldn’t work anyways since model state validation is only triggered during runtime. It is up to integration tests to check if the model binding works properly.
What we are going to do here instead is add the ModelError
object explicitly to the ModelState
and then assert on the response of the called method.
Remove method
Testing the remove method is pretty straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
[Fact] public void Remove_NotExistingGuidPassed_ReturnsNotFoundResponse() { // Arrange var notExistingGuid = Guid.NewGuid(); // Act var badResponse = _controller.Remove(notExistingGuid); // Assert Assert.IsType<NotFoundResult>(badResponse); } [Fact] public void Remove_ExistingGuidPassed_ReturnsOkResult() { // Arrange var existingGuid = new Guid("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200"); // Act var okResponse = _controller.Remove(existingGuid); // Assert Assert.IsType<OkResult>(okResponse); } [Fact] public void Remove_ExistingGuidPassed_RemovesOneItem() { // Arrange var existingGuid = new Guid("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200"); // Act var okResponse = _controller.Remove(existingGuid); // Assert Assert.Equal(2, _service.GetAllItems().Count()); } |
Remove method tests take care that valid response is returned and that object is indeed removed from the list.
Summary
This concludes the tests scenarios for our ShoppingCartController
and we just want to summarize the general advice about unit testing. There are few guidelines or best practices you should strive for when writing unit tests. Respecting these practices will certainly make your (and life of your fellow developer) easier.
Unit tests should be readable
No one wants to spend time trying to figure out what is that your test does. Ideally, this should be clear just by looking at the test name.
Unit tests should be maintainable
We should try to write our tests in a way that minor changes to the code shouldn’t make us change all of our tests. The DRY (don’t repeat yourself) principle applies here, and we should treat our test code the same as the production code. This lowers the possibility that one day someone gets to the point where he/she needs to comment out all of our tests because it has become too difficult to maintain them.
Unit sets should be fast
If tests are taking too long to execute, it is probable that people will run them less often. That is certainly a bad thing and no one wishes to wait too long for tests to execute.
Unit tests should not have any dependances
It is important that anyone who is working on the project can execute tests without the need to provide access to some external system or database. Tests need to run in full isolation.
Make tests trustworthy rather than just aiming for the code coverage
Good tests should provide us with the confidence that we will be able to detect errors before they reach production. It is easy to write tests that don’t assert the right things just to make them pass and to increase code coverage. But there is no point in doing that. We should try to test the right things to be able to rely on them when time comes to make some changes to the code.
Conclusion
In this post, we’ve learned what unit testing is and how to set up the unit testing project with xUnit.
We’ve also learned the basic scenarios of testing the controller logic on some CRUD operations.
These examples should give you a great starting point for writing your own unit tests, and test the more complex projects.
Thanks for reading and hopefully this article will help you grasp the unit concepts and unit testing in ASP.NET Core a little bit better.
If you have enjoyed reading this article and if you would like to receive the notifications about the freshly published .NET Core content we encourage you to subscribe to our blog.
Milos, thanks for the article.
Unfortunately, the base set of HTTP methods covers Update (PUT) method as well, but it isn’t considered in the article.
Thank you barabasishe, I am glad you liked the article.
You are right as the PUT request was not covered in the post, and the logic would be very similar as for the testing of the POST request.
The difference is that PUT request returns 204 HTTP response for the happy path, so I would assert that appropriate NoContentResult object was returned if the request was valid. If you are building a public API, might be a good idea to also verify that BadRequest response was returned when trying to update the non-existing item.
Hope this helps,
Milos
IsType and Equal do not exist and are not in the documentation for Assert: https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.assert?view=mstest-net-1.2.0.
I had to use: Assert.IsInstanceOfType(object, type); and change Assert.Equal to Assert.Equals. Where did you get IsType from? Is that from an external library or an extension method? Also, you never created the class: ShoppingCartServiceFake. If you search the entire page there is only 1 result. What exactly is ShoppingCartServiceFake?
I have learned a lot. Thanks for making this!
Hi Preston, I am happy liked the post.
If you take a look at the section ‘Creating a Testing Project’ , you will see that we are using xUnit testing tools, and library provided has all the methods you were missing.
In the same section there is a typo. Class named ShoppingCartService should be named ShoppingCartServiceFake, and you can see this if you take a look at the code from a git repo:
https://github.com/CodeMazeBlog/unit-testing-aspnetcore-webapi
Thanks for you comments, we will fix that in the post soon.
My apologies, you’re right I did start an MSTest Unit Test project instead. Though the functionality was basically the same, I for whatever reason didn’t connect the dots.