Unit Testing ASP.NET Core Applications

Abstract: Unit Tests are a powerful tool available to any developer. In this series of articles, we we will see how to add unit tests to a simple example ASP.NET Core project. We will go through an automated testing strategy involving unit tests, integration tests, end–to-end tests and load tests.

 

If you want to make sure your application behaves as expected, then you need to test it. There is no other way around it.

However, this is easier said than done!

There are quite a few different testing methods, each with its own unique strengths. Each method concentrates on testing different aspects of your application.

In order to truly test your application, you will need to devise a strategy that combines these testing techniques. Getting to know these methods should also make it easier to adopt development processes like Test Driven Development (TDD) or Behavior Driven Development (BDD), should you decide so.

In this series of articles, we will revisit an earlier project about writing clean frontend code in ASP.NET Core and use it as our test subject while we go through an automated testing strategy involving unit tests, integration tests, end–to-end tests and load tests.

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.

Before diving into these automated testing methods, I cannot finish this introduction without stressing the importance of complementing any automated testing strategy with some manual exploratory testing, preferably by QA specialists!

The code discussed through the article is available on its GitHub repo.

The Automated Testing Strategy

As you consider automating the testing of your application, you need to carefully think how you are going to combine the different testing methods in order to cover as many different aspects of your application as possible.

Let’s start with a quick recap on these testing methods and their strengths and weaknesses.

This might be old news for some of you, so feel free to skip into the next section titled “The Test Subject” which introduces the test subject project.

Unit Testing

Unit Testing concentrates on exercising an individual unit isolated from the rest of the system. It mocks or stubs its dependencies to make sure it produces the desired outcomes, given a well-known set of inputs or system state.

We typically consider our classes as units, testing their public methods while mocking their dependencies. This has the additional benefit of leading your code towards a loosely coupled design, as otherwise you won’t be able to isolate your classes from their dependencies.

We also can’t ignore the importance of being able to write and run code without having to start the entire application

  • You will get the most benefits from them on methods rich in business logic, where you want to exercise different inputs, corner cases or error conditions.
  • However, in areas of your code that concentrate on dealing with external dependencies and infrastructure (like databases, files, services, 3rd party libraries/frameworks), these tests add considerably less value. Most of what this code does is dealing with external dependencies that you will be mocking in your tests!
  • Hence the importance of a testing strategy that combines different methods!

When it comes to running your unit tests, they are fast and require no special configuration on your Continuous Integration (CI) servers.

Integration Testing

These tests will exercise several units together, treating them as a black box which is provided with some inputs, and should produce the expected outcomes.

In the context of web applications, it is common to consider your web server as the black box, writing integration tests that exercise its public endpoints without the need for a browser or client-side code.

This means your integration tests will exercise your real web server code with real HTTP requests, while it will be up to you to decide whether to include external dependencies like databases within your test or not.

When providing REST APIs, these tests are extremely valuable.

Since web servers need to be started, databases need to be isolated or reset and seed, and each test involves real HTTP requests; we can safely consider integration tests as more expensive to write and maintain, than unit tests. They are also slower to run and are harder to debug when they fail.

That’s why they are good candidates to ensure that the most important and common use cases for each endpoint works with your real server.

At the same time, Integration tests are not so apt for trying all the different scenario variations, corner cases, error and boundary conditions; which is something you want to leave for your unit tests.

End–to-End Testing

End-to-End tests will exercise your system using the same interface than any regular user, through the UI. These makes them the most expensive tests to write and maintain, as they are tightly coupled to your UI, breaking often, as a result of changes to your interface.

Again, in the context of web applications, these tests will interact with a browser the same way that a user would do, using frameworks like Selenium to drive the browser. This makes these tests the slowest to run and more expensive to write and maintain! As you can imagine, investigating a failure in one of these tests is pretty close (if not the same) to debugging the application.

However, they are great to run smoke tests or golden thread tests on real browsers and devices. You want these tests to verify that your most important scenarios are not broken from the user perspective.

Other ways of testing your code

The testing methods we just discussed concentrate on ensuring that the application behaves as expected. But that’s not the end of it, since your application might produce the desired output but may still have flaws like high response times or security issues.

That’s why you can complement your testing strategy with additional methods like Load Testing, Penetration Testing or Static Code Analysis.

Automated testing is a huge subject that goes beyond unit, integration and end–to-end tests.

Stay tuned to read about Load Testing in the last article of the series (coming soon).

Finally, don’t ignore the power of manual exploratory testing, especially when done by a seasoned QA engineer. They will not only catch bugs on scenarios you didn’t anticipate, but will also ensure your application behaves closer to the way a real user would expect or want to.

The Testing Pyramid

One of the main points to take from this overview is that different types of tests have different implementation/maintenance costs and have different purposes. That’s why people like Martin Fowler have been talking for quite some time about the concept of the testing pyramid.

Figure 1, the Testing Pyramid

As you can see, it is a graphical depiction of a testing strategy that plays the strengths and weaknesses of the different testing methods.

  • At the bottom you have a large suite of unit tests that exhaustively tests every piece of code with meaningful business logic. These provide constant feedback to developers about their code changes and are constantly run by CI servers.
  • On the next level, you have a reduced number of integration tests, making sure several of your units working together still produce the expected results (In web applications, this would be your web server without client-side code). Developers typically run these at specific moments like adding a new feature to the system or before committing a change. CI servers will either run these on every change or after deploying a staging/integration environment, depending on the exact nature and implementation of the tests and how external dependencies like databases were isolated.
  • Finally, at the top, you have a very exclusive number of End-to-End tests that make sure your application isn’t fundamentally broken from a real user perspective. These will mostly be run by CI servers after environments are deployed, while developers will run them even more infrequently than integration tests.

In the remaining article, we will see how to write unit tests for our test subject.

Note: The next articles in the series will cover the upper levels of the testing strategy.

The Test Subject

As mentioned previously, we will use an earlier project that talks about writing clean frontend code in ASP.NET Core and use it as our test subject. The application is a blogging site where authenticated users can write new blog posts and everyone else can read them.

Figure 2, The example blogging application used as Test Subject

Technically, it is a standard ASP.NET Core web application which has been upgraded to ASP.NET Core 2.0 and has no tests at all!

This is a simple application logic-wise, but it will serve as a good example for writing the different types of tests without getting lost in the subtleties of the domain, business rules and use cases. Fear not, it will be enough to highlight many of the common challenges you will face when writing these tests.

Writing Unit Tests for ASP.NET Core applications

We will start from the bottom of the pyramid, adding unit tests to our sample blogging application.

In order to write and run the unit tests, we will be using:

  • xUnit as the test framework
  • Moq as the mocking framework
  • A new project that will contain all of our unit tests

The first thing we need is a new unit test project where we can write the unit tests!

Note: If this was a real application with no tests, it would be more interesting to first add integration and end–to-end tests! As unit tests would most likely require code changes in order to make your code testable, you would have the other tests as a safety net, while you refactor the application.

The reason why I am not following that approach is because I believe it is easier to introduce the concepts by first looking at unit tests, understand its strengths and shortcomings, highlighting the need for higher level tests, before moving into integration and end-to-end tests.

In Visual Studio, right click your solution and select Add > New Project, then select xUnit Test Project.

Since the project we want to test is named BlogPlayground, name the new test project BlogPlayground.Test. Alternatively, you can run from the command line…

mkdir BlogPlayground.Test && cd BlogPlayground.Test && dotnet new xunit

...to achieve the same results.

Note: If you are wondering what’s the difference between the xUnit Test Project and Unit Test Project templates, it all boils down to the test framework used. The first one uses xUnit while the latter uses MSTest. If you prefer MSTest to xUnit, you can still follow along, as the principles are the same. You will just need to adjust your test code for the differences between the frameworks like attributes and setup/teardown code.

Let’s finish setting up our test project by adding a reference to BlogPlayground (since our test code will use the real code) and installing the NuGet package Moq (which will be needed pretty soon to mock dependencies).

We will focus on adding unit tests for the ArticlesController class, so rename the generated test class as ArticlesControllerTest and move it inside a new folder Controller inside the test project.

Your solution explorer should look like Figure 3 at this stage (notice how the structure of the Unit Test project matches that of the real project):

Figure 3, Solution after adding the new Unit Testing project

We are ready to write our first test for the ArticlesController! Let’s start by adding a test that ensures the Index method renders the expected view with the articles list as its model.

However, just by inspecting its code it is obvious we have hit our first blocker:

How can we create an instance of ArticlesController without a db context and an UserManager? And if we were to test the Index method, how can we mock the context so we can test the Index method?

public ArticlesController(ApplicationDbContext context, UserManager<ApplicationUser> userManager)

{

    _context = context;

    _userManager = userManager;

}

 

public async Task<IActionResult> Index()

{

    var applicationDbContext = _context.Article.Include(a => a.Author);

    return View(await applicationDbContext.ToListAsync());

}

 

// Other simpler methods omitted here!

 

public async Task<IActionResult> Create([Bind("Title, Abstract,Contents")] Article article)

{

    if (ModelState.IsValid)

    {

        article.AuthorId = _userManager.GetUserId(this.User);

        article.CreatedDate = DateTime.Now;

        _context.Add(article);

        await _context.SaveChanges();

        return RedirectToAction("Index");

    }

    return View(article);

}

public async Task<IActionResult> DeleteConfirmed(int id)

{

    var article = await _context.Article.SingleOrDefaultAsync(m => m.ArticleId == id);

    _context.Article.Remove(article);

    await _context.SaveChanges();

    return RedirectToAction("Index");

}

Our class right now is tightly coupled to the ApplicationDbContext and UserManager classes, and we will need to refactor our code to ensure it is loosely coupled and testable. Otherwise we won’t be able to test it unless we provide real instances of those dependencies, in which case we would end up running the tests against a real db context connected to a database.

Refactoring towards a loosely coupled design

Let’s start by refactoring our database access code so that our controller doesn’t depend directly on the ApplicationDbContext. While this was fine within the context of the original application, typically most applications would have some layer(s) between the controller and the data access code, like a service layer, business layer or repository layer.

Figure 4, refactoring towards a loosely coupled design

There are many ways of breaking the dependency between the controller and the context classes, but the important message here is that you need to introduce an abstraction between these two classes.

Note: As you might have noticed, this is pushing our tight coupling down to a new class! However, this is fine as long as you keep the code that really matters to your users - the one that implements your business logic, separated from infrastructure and plumbing code. Remember we will still have other tests like integration and end–to-end that verify everything works when put together!

In this article, we will create a simple IArticlesRepository interface that our controller will depend upon.

You can find many great discussions about the different implementations of the generic repository pattern and unit of work. While there are better implementations, I have decided to keep things simple and to-the-point - the point breaking the dependencies in tightly coupled code!

public interface IArticlesRepository

{

    Task<List<Article>> GetAll();

    Task<Article> GetOne(int id);

    void Add(Article article);

    void Remove(Article article);

    Task SaveChanges();

}

The implementation is pretty straightforward. We are adding a new ArticlesRepository class that depends on the ApplicationDbContext:

public class ArticlesRepository : IArticlesRepository

{

    private readonly ApplicationDbContext _context;

 

    public ArticlesRepository(ApplicationDbContext context)

    {

        _context = context;

    }

 

    public Task<List<Article>> GetAll() =>

        _context.Article.Include(a => a.Author).ToListAsync();

 

    public Task<Article> GetOne(int id) =>

            _context.Article.Include(a => a.Author)

.SingleOrDefaultAsync(m => m.ArticleId == id);

 

    public void Add(Article article) =>

        _context.Article.Add(article);

 

    public void Remove(Article article) =>

        _context.Article.Remove(article);       

 

    public Task SaveChanges() =>

        _context.SaveChangesAsync();

}

Now we can modify the controller so it depends on our new abstraction, the IArticlesRepository interface. If you have any trouble modifying the methods not shown here, please check the source code on GitHub:

private readonly IArticlesRepository _articlesRepository;

private readonly UserManager<ApplicationUser> _userManager;

 

public ArticlesController(IArticlesRepository articlesRepository, UserManager<ApplicationUser> userManager)

{

    _articlesRepository = articlesRepository;

    _userManager = userManager;

}

 

public async Task<IActionResult> Index()

{

    return View(await _articlesRepository.GetAll());

}

 

// Other simpler methods omitted here!

 

public async Task<IActionResult> Create([Bind("Title, Abstract,Contents")] Article article)

{

    if (ModelState.IsValid)

    {

        article.AuthorId = _requestUserProvider.GetUserId();

        article.CreatedDate = DateTime.Now;

        _articlesRepository.Add(article);

        await _articlesRepository.SaveChanges();

        return RedirectToAction("Index");

    }

    return View(article);

}

public async Task<IActionResult> DeleteConfirmed(int id)

{

    var article = await _articlesRepository.GetOne(id);

    _articlesRepository.Remove(article);

    await _articlesRepository.SaveChanges();

    return RedirectToAction("Index");

}

Don’t forget to register it on your startup method as a scoped dependency. Otherwise your app would fail at runtime and your integration tests would fail! (See the importance of the different testing methods?)

// Add repos as scoped dependency so they are shared per request.

services.AddScoped<IArticlesRepository, ArticlesRepository>();

We are almost done!

The controller is still tightly coupled to the UserManager class used to get the id of the user making the request, so it can be saved as the AuthorId field when creating a new article. We can follow the same approach and introduce a new abstraction:

public class RequestUserProvider: IRequestUserProvider

{

    private readonly IHttpContextAccessor _contextAccessor;

    private readonly UserManager<ApplicationUser> _userManager;

 

    public RequestUserProvider(IHttpContextAccessor contextAccessor, UserManager<ApplicationUser> userManager)

    {

        _contextAccessor = contextAccessor;

        _userManager = userManager;

    }

 

    public string GetUserId()

    {

        return _userManager.GetUserId(_contextAccessor.HttpContext.User);

    }

}

Updating the controller code to use this new interface should be trivial, but you can always check the code in GitHub.

Our first tests mocking the dependencies

Finally, we are ready to start writing tests since the ArticlesController now depends only on two interfaces that we can mock in our tests.

Let’s go back to our ArticlesControllerTest class and let’s update it so that in every test we get a fresh controller instance with mocks injected, using Moq to create and setup the mocks:

public class ArticlesControllerTest

{

    private Mock<IArticlesRepository> articlesRepoMock;

    private Mock<IRequestUserProvider> requestUserProviderMock;

    private ArticlesController controller;

 

    public ArticlesControllerTest()

    {

        articlesRepoMock = new Mock<IArticlesRepository>();

        requestUserProviderMock = new Mock<IRequestUserProvider>();

        controller = new ArticlesController(articlesRepoMock.Object, requestUserProviderMock.Object);

    }

 

    [Fact]

    public void IndexTest_ReturnsViewWithArticlesList()

    {

 

    }

}

This setup is important, as it ensures every individual test gets its own instance of the controller with fresh mocks. It will also help ensuring our test code stays clean and maintainable.

Writing good unit tests is an art in itself. If you need some directions, I would recommend reading the book The Art of Unit Testing by Roy Osherove, but by all means, feel free to do your own research and reading!

Let’s now implement the first test, verifying that the Index method will render the expected view with the expected articles list.

// GET: Articles

public async Task<IActionResult> Index()

{

    return View(await _articlesRepository.GetAll());

}

The test follows the classic arrange/act/assert pattern so it contains these clear sections that arrange any data or mocks required, execute the code under test (act) and perform assertions based on the result:

[Fact]

public async Task IndexTest_ReturnsViewWithArticlesList()

{

    // Arrange

    var mockArticlesList = new List<Article>

    {

        new Article { Title = "mock article 1" },

        new Article { Title = "mock article 2" }

    };

    ArticlesRepoMock

        .Setup(repo => repo.GetAll())

        .Returns(Task.FromResult(mockArticlesList));           

 

    // Act

    var result = await controller.Index();

 

    // Assert

    var viewResult = Assert.IsType<ViewResult>(result);

    var model = Assert.IsAssignableFrom<IEnumerable<Article>>(viewResult.ViewData.Model);

    Assert.Equal(2, model.Count());

}

I hope you find it easy to follow, but we are basically setting the behavior of our IArticlesRepository mock so that it returns a predefined list of articles. We then call the controller’s Index method and verify that its result is a ViewResult with a model that is an IEnumerable<Article> with two elements.

Now run the tests, for example using Ctrl+R,A in Visual Studio or dotnet test from the command line.

Congratulations, you have written and executed the first test!

Figure 5, running the first unit test from visual studio

Figure 6, running the unit test from the command line using dotnet test

Additional tests

Let’s now move our attention to the Details controller method. This one is a bit more interesting since there is some logic involved in making sure we can find an article for the given id:

// GET: Articles/Details/5

public async Task<IActionResult> Details(int? id)

{

    if (id == null)

    {

        return NotFound();

    }

 

    var article = await _articlesRepository.GetOne(id.Value);

    if (article == null)

    {

        return NotFound();

    }

 

    return View(article);

}

We can easily add a test where we supply id as null and verify that we get a NotFoundResult as the response:

[Fact]

public async Task DetailsTest_ReturnsNotFound_WhenNoIdProvided()

{

    // Act

    var result = await controller.Details(null);

 

    // Assert

    var viewResult = Assert.IsType<NotFoundResult>(result);

}

With a little help from our repository mock, we can add another similar test that verifies we get another NotFoundResult when there is no article in the repository with the given id:

[Fact]

public async Task DetailsTest_ReturnsNotFound_WhenArticleDoesNotExist()

{

    // Arrange

    var mockId = 42;

    articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult<Article>(null));

 

    // Act

    var result = await controller.Details(mockId);

 

    // Assert

    var viewResult = Assert.IsType<NotFoundResult>(result);

}

Finally, we can complete the coverage of the controller’s Details method by verifying that the controller renders a view passing the article from the repository, in case we find an article with the given id:

[Fact]

public async Task DetailsTest_ReturnsDetailsView_WhenArticleExists()

{

    // Arrange

    var mockId = 42;

    var mockArticle = new Article { Title = "mock article" };

    articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle));

 

    // Act

    var result = await controller.Details(mockId);

 

    // Assert

    var viewResult = Assert.IsType<ViewResult>(result);

    Assert.Equal(mockArticle, viewResult.ViewData.Model);

}

As you can see, we had to do some groundwork to ensure our class was testable. But once you get to that stage, testing your class is really straightforward.

Before we move on, we are going to write tests for the Create POST method (a test for the GET method would be trivial). The code for it is as follows:

// POST: Articles/Create

[HttpPost]

[ValidateAntiForgeryToken]

[Authorize]

public async Task<IActionResult> Create([Bind("Title, Abstract,Contents")] Article article)

{

    if (ModelState.IsValid)

    {

        article.AuthorId = _requestUserProvider.GetUserId();

        article.CreatedDate = DateTime.Now;

        _articlesRepository.Add(article);

        await _articlesRepository.SaveChanges();

        return RedirectToAction("Index");

    }

    return View(article);

}

We can start by verifying what happens when the model state is not valid. This is as simple as manually setting the controller’s ModelState property before executing the Create method:

[Fact]

public async Task CreateTest_Post_ReturnsCreateView_WhenModelStateIsInvalid()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    controller.ModelState.AddModelError("Description", "This field is required");

 

    // Act

    var result = await controller.Create(mockArticle);

 

    // Assert

    var viewResult = Assert.IsType<ViewResult>(result);

    Assert.Equal(mockArticle, viewResult.ViewData.Model);

    articlesRepoMock.Verify(repo => repo.Add(mockArticle), Times.Never());

}

Did you observe that the specific assertion verifying the repo wasn’t called?

I am adding this because we are writing the tests AFTER the controller code was written. If you follow a TDD process, you would never add code unless a test requires it, which means you won’t need to add verifications for things that shouldn’t happen!

Next, let’s make sure the article gets added to the repository and we are redirected to the index view:

[Fact]

public async Task CreateTest_Post_AddsArticleToRepository_AndRedirectsToIndex()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask);

 

    // Act

    var result = await controller.Create(mockArticle);

 

    // Assert

    articlesRepoMock.Verify(repo => repo.Add(mockArticle));

    var viewResult = Assert.IsType<RedirectToActionResult>(result);

    Assert.Equal("Index", viewResult.ActionName);

}

The remaining logic to be covered would be ensuring both the AuthorId and CreatedDate are set in the article that gets saved to the database. Both present a unique interesting challenge since the author is retrieved from the other mock and the creation date depends on the current time:

[Fact]

public async Task CreateTest_Post_SetsAuthorId_BeforeAddingArticleToRepository()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    var mockAuthorId = "mockAuthorId";

    articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask);

    requestUserProviderMock.Setup(provider => provider.GetUserId()).Returns(mockAuthorId);

 

    // Act

    var result = await controller.Create(mockArticle);

 

    // Assert

    articlesRepoMock.Verify(repo =>

        repo.Add(It.Is<Article>(article =>

            article == mockArticle

            && article.AuthorId == mockAuthorId)));

}

 

[Fact]

public async Task CreateTest_Post_SetsCreatedDate_BeforeAddingArticleToRepository()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    var startTime = DateTime.Now;

    articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask);

 

    // Act

    var result = await controller.Create(mockArticle);

    var endTime = DateTime.Now;

 

    // Assert

    articlesRepoMock.Verify(repo =>

        repo.Add(It.Is<Article>(article =>

            article == mockArticle

            && article.CreatedDate >= startTime

            && article.CreatedDate <= endTime)));

}

You should be able to follow the same steps in order to test the remaining methods, but please check the source code in GitHub if you run into trouble.

Testing API Controllers

The Controller we have tested previously is an MVC controller which returns views and expects form data as input. Let’s see an example of how to test an API controller. Since the project didn’t have any, let’s begin by adding one!

[Produces("application/json")]

[Route("api/articles")]

public class ArticlesApiController : Controller

{

    private readonly IArticlesRepository _articlesRepository;

    private readonly IRequestUserProvider _requestUserProvider;

 

    public ArticlesApiController(IArticlesRepository articlesRepository, IRequestUserProvider requestUserProvider)

    {

        _articlesRepository = articlesRepository;

        _requestUserProvider = requestUserProvider;

    }

 

    [HttpGet()]

    public async Task<IEnumerable<Article>> GetArticles()

    {

        return await _articlesRepository.GetAll();

    }

 

    [HttpGet("{id}")]

    public async Task<ActionResult> GetArticle(int id)

    {

        var article = await _articlesRepository.GetOne(id);

        if (article == null) return NotFound();

 

        return Ok(article);

    }

 

    [HttpPost()]

    [ValidateAntiForgeryToken]

    [Authorize]

    public async Task<ActionResult> AddArticle([FromBody]Article article)

    {

        if (!ModelState.IsValid) return BadRequest(ModelState);

             

        article.AuthorId = _requestUserProvider.GetUserId();

        article.CreatedDate = DateTime.Now;

        _articlesRepository.Add(article);

        await _articlesRepository.SaveChanges();

        return Ok(article);

    }

 

    [HttpDelete("{id}")]

    [ValidateAntiForgeryToken]

    [Authorize]

    public async Task<ActionResult> DeleteArticle(int id)

    {

        var article = await _articlesRepository.GetOne(id);

        if (article == null) return NotFound();

 

        _articlesRepository.Remove(article);

        await _articlesRepository.SaveChanges();

 

        return NoContent();

    }       

}

It is a fairly standard API controller. It provides a similar functionality to the previous controller intended to be used as a REST API, that sends and receive JSON data.

If you read through the code, you will notice we have again used dependency injection and the IArticlesRepository and IRequestUserProvider abstractions so we can unit test it as well.

That means we can write tests in the same way we did before. For example, this is how we would write the first simple test that verifies that the GetArticles method works as expected:

private Mock<IArticlesRepository> articlesRepoMock;

private Mock<IRequestUserProvider> requestUserProviderMock;

private ArticlesApiController controller;

 

public ArticlesApiControllerTest()

{

    articlesRepoMock = new Mock<IArticlesRepository>();

    requestUserProviderMock = new Mock<IRequestUserProvider>();

    controller = new ArticlesApiController(articlesRepoMock.Object, requestUserProviderMock.Object);

}

[Fact]

public async Task GetArticlesTest_RetursArticlesList()

{

    // Arrange

    var mockArticlesList = new List<Article>

    {

        new Article { Title = "mock article 1" },

        new Article { Title = "mock article 2" }

    };

    articlesRepoMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(mockArticlesList));           

 

    // Act

    var result = await controller.GetArticles();

 

    // Assert

    Assert.Equal(mockArticlesList, result);

}

I hope it is evident that we are structuring our tests in exactly the same way, with the arrange/act/assert stages and we keep mocking our dependencies as before.

Let’s see something a bit more interesting, like the tests for the AddArticle method which returns an ActionResult:

[Fact]

public async Task AddArticleTest_ReturnsBadRequest_WhenModelStateIsInvalid()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    controller.ModelState.AddModelError("Description", "This field is required");

 

    // Act

    var result = await controller.AddArticle(mockArticle);

 

    // Assert

    var actionResult = Assert.IsType<BadRequestObjectResult>(result);

    Assert.Equal(new SerializableError(controller.ModelState), actionResult.Value);

}

 

[Fact]

public async Task AddArticleTest_ReturnsArticleSuccessfullyAdded()

{

    // Arrange

    var mockArticle = new Article { Title = "mock article" };

    articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask);

 

    // Act

    var result = await controller.AddArticle(mockArticle);

 

    // Assert

    articlesRepoMock.Verify(repo => repo.Add(mockArticle));

    var actionResult = Assert.IsType<OkObjectResult>(result);

    Assert.Equal(mockArticle, actionResult.Value);

}

There is nothing new in the preceding code, other than adapting our tests to the different ActionResult return types. As an exercise, you can try completing the coverage of the controller.

Check the code on its GitHub repo if you run into trouble.

Client-side Unit Tests

If you are writing a web application, it is very likely that your client-side JavaScript code is as complex (if not more) than your server-side code.

This means you will want to give it the same test treatment than your server-side code will receive. While we won’t be covering this in the article, the principles stay the same and only the tooling changes!

You will still need:

  • A test framework like Mocha or Jasmine (depending on whether your tests run purely with Node.js or require a browser, even a headless one like Phantom.js)
  • A mocking library like Sinon.JS and a tool like proxyquire to replace module dependencies with mocks.
  • More importantly, you still need to isolate your units from its dependencies and write readable and maintainable tests.

The reason why I am not covering client-side testing here is that the actual approach and tooling is greatly influenced by the client-side framework you are using. I would encourage you to do some research and add unit tests adapted to your framework of choice. The dynamic nature of JavaScript makes them even easier!

Conclusion

Unit Tests are a powerful tool available to any developer. Having good unit tests covering the logic intensive areas of your application, will lead you towards a better design and will allow you to work without being afraid of modifying your code.

This ability to exercise your code without having to fully start your application cannot be overstated enough and will shine even more when trying to reproduce and fix a bug. Once you get used to working in projects with tests, moving to one without them, feels like a huge step backwards!

But don’t be deceived! Writing good unit tests is an art of which I merely brushed a few strokes.

There is a learning curve, but it’s really worth it! Bad unit tests will do more harm than good ones and end up slowing you down, without adding much real value.

During the article we have added unit tests to a simple example ASP.NET Core project that provided a very straightforward API to manage blog posts. Although useful to showcase how to make your code testable and how to write unit tests, I would be really surprised if by this stage you didn’t have the following concerns given the effort required:

  • How can you make sure that the simple repository class you added works as expected?
  • What happens with all the attributes you use in your controller class and methods that greatly affect the final behavior of your application in areas like routing, binding or security?
  • How can these tests catch simple errors, like the one where you forget to register the new interfaces within the ConfigureServices method of your Startup class?
  • Is the effort and discipline required to keep your code testable and to write and maintain these unit tests, worth it?

That’s why besides unit tests, you also need to consider integration and end-to-end tests in your testing strategy.

You want full coverage with unit tests on the areas of your application that are rich in logic, particularly the core of your application where your specific business rules live. Dealing with external dependencies, infrastructure code and plumbing is pushed to the boundaries from this core via the right abstractions.

Higher level tests like integration and end-to-end tests will make sure that your entire application works together as expected. If unit tests were done right, you will just need to test a reduced number of scenarios to make sure your code and its dependencies play along as expected.

Remember that tests are not free, so make sure you get the right value from each kind!

Add comment