Published: March 14 2023

.NET + VS Code + XUnit - Setup Unit Testing & Code Coverage in ASP.NET Core

Tutorial built with .NET 7.0 and XUnit

Unit testing, it rarely makes the headlines but it's something we all have to do as developers.

I recently added unit testing and code coverage to a .NET 7.0 (ASP.NET Core) project in VS Code. This post covers all the steps I went through to set everything up.

The end result is a pair of .NET projects, one for the application being tested (an example API) and one for the unit tests, with VS Code configured to run the tests and generate code coverage reports at the click of a button.


Tutorial Contents


Tools required for the the .NET unit testing tutorial

To develop and run .NET 7.0 applications locally, download and install the following:

  • .NET SDK - includes the .NET runtime and command line tools
  • Visual Studio Code - code editor that runs on Windows, Mac and Linux
  • C# extension for Visual Studio Code - adds support to VS Code for developing .NET applications


Download example .NET API project to test

We'll start with an example .NET 7 CRUD API I posted recently that doesn't have any unit tests yet. The details of the API project aren't required for this tutorial, but if you're interested see .NET 7.0 + Dapper + SQLite - CRUD API Tutorial in ASP.NET Core.

  1. Create a new root folder for the solution named dotnet-7-dapper-sqlite-crud-api
  2. Inside the new root folder, download or clone the .NET project from https://github.com/cornflourblue/dotnet-7-dapper-sqlite-crud-api into a folder named WebAPI, e.g. with the following git command:
    git clone https://github.com/cornflourblue/dotnet-7-dapper-sqlite-crud-api.git WebAPI
  3. cd into the WebAPI folder and restore the project NuGet packages with the following dotnet command:
    dotnet restore


Command output

The git clone output should look something like this:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api> git clone https://github.com/cornflourblue/dotnet-7-dapper-sqlite-crud-api.git WebAPI
Cloning into 'WebAPI'...
remote: Enumerating objects: 26, done.
remote: Counting objects: 100% (26/26), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 26 (delta 1), reused 26 (delta 1), pack-reused 0
Unpacking objects: 100% (26/26), 7.36 KiB | 27.00 KiB/s, done.


And this is the output from dotnet restore:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI> dotnet restore
  Determining projects to restore...
  Restored C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI\WebApi.csproj (in 428 ms).


Create .NET unit test project with XUnit

These steps create a new unit test project and add a reference to the project we want to test.

  1. Inside the root folder (/dotnet-7-dapper-sqlite-crud-api), create a new folder for the unit test project named WebAPI.Tests
  2. From the command line, cd into the /dotnet-7-dapper-sqlite-crud-api/WebAPI.Tests folder and run the following command to create the unit test project with XUnit:
    dotnet new xunit
  3. From the same folder run the following command to add a reference to the WebAPI project from the unit test project:
    dotnet add reference ..\WebAPI\WebApi.csproj
  4. Delete the auto generated test file UnitTest1.cs


Command output and screenshots

The dotnet new xunit output should look something like this:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests> dotnet new xunit
The template "xUnit Test Project" was created successfully.

Processing post-creation actions...
Restoring C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj:
  Determining projects to restore...
  Restored C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj (in 1.56 sec).
Restore succeeded.


This is the dotnet add reference output:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests> dotnet add reference ..\WebAPI\WebApi.csproj
Reference `..\WebAPI\WebApi.csproj` added to the project.


And this is how our projects should look in the VS Code file explorer now:


Add some .NET unit tests

Here we install a couple of NuGet packages (Moq and AutoFixture) that help with setting up test data and mocking dependencies, and add a few initial unit tests for the UserService class in the example API project.

  1. Install the AutoFixture package from NuGet with the following command:
    dotnet add package AutoFixture
  2. Install the Moq package from NuGet with the following command:
    dotnet add package Moq
  3. Create a new folder named Services in the WebAPI.Tests project
  4. Create a new file named UserServiceTests.cs in the Services folder with the following tests:
    using AutoFixture;
    using AutoMapper;
    using Moq;
    using WebApi.Entities;
    using WebApi.Helpers;
    using WebApi.Repositories;
    using WebApi.Services;
    
    namespace WebApi.Tests.Services;
    
    public class UserServiceTests
    {
        private IUserService _userService;
        private Mock<IUserRepository> _mockUserRepository;
        private Fixture _fixture;
    
        public UserServiceTests()
        {
            // fixture for creating test data
            _fixture = new Fixture();
    
            // mock user repo dependency
            _mockUserRepository = new Mock<IUserRepository>();
    
            // automapper dependency
            var mapper = new MapperConfiguration(x => x.AddProfile<AutoMapperProfile>()).CreateMapper();
    
            // service under test
            _userService = new UserService(_mockUserRepository.Object, mapper);
        }
    
        [Fact]
        public async Task GetAll_ReturnsAllUsers()
        {
            // Arrange
            var usersFixture = _fixture.CreateMany<User>(3);
            _mockUserRepository.Setup(x => x.GetAll()).ReturnsAsync(usersFixture);
    
            // Act
            var users = await _userService.GetAll();
    
            // Assert
            Assert.True(users.Count() == 3);
            Assert.Equal(usersFixture.First().FirstName, users.First().FirstName);
        }
    
        [Fact]
        public async Task GetById_ValidId_ReturnsUser()
        {
            // Arrange
            var userFixture = _fixture.Create<User>();
            var id = userFixture.Id;
            _mockUserRepository.Setup(x => x.GetById(id)).ReturnsAsync(userFixture);
    
            // Act
            var user = await _userService.GetById(id);
    
            // Assert
            Assert.NotNull(user);
            Assert.Equal(userFixture.FirstName, user.FirstName);
        }
    
        [Fact]
        public async Task GetById_InvalidId_ThrowsException()
        {
            // Act & Assert
            await Assert.ThrowsAsync<KeyNotFoundException>(() => _userService.GetById(100));
        }
    }


Command output and screenshots

Output from dotnet add package AutoFixture:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests> dotnet add package AutoFixture
  Determining projects to restore...
  Writing C:\Users\Jason\AppData\Local\Temp\tmp564F.tmp
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : Adding PackageReference for package 'AutoFixture' into project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/autofixture/index.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/autofixture/index.json 300ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/2.0.0/3.6.2.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/2.0.0/3.6.2.json 945ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.6.3/3.24.6.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.6.3/3.24.6.json 1004ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.25.0/3.47.0.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.25.0/3.47.0.json 229ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.47.1/4.18.0.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/autofixture/page/3.47.1/4.18.0.json 1018ms
info : Restoring packages for C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj...
info : Package 'AutoFixture' is compatible with all the specified frameworks in project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : PackageReference for package 'AutoFixture' version '4.18.0' added to file 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : Generating MSBuild file C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\WebAPI.Tests.csproj.nuget.g.targets.
info : Writing assets file to disk. Path: C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\project.assets.json
log  : Restored C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj (in 449 ms).


Output from dotnet add package Moq:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests> dotnet add package Moq
  Determining projects to restore...
  Writing C:\Users\Jason\AppData\Local\Temp\tmpEF06.tmp
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : Adding PackageReference for package 'Moq' into project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/moq/index.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/moq/index.json 625ms
info : Restoring packages for C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj...
info : Package 'Moq' is compatible with all the specified frameworks in project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : PackageReference for package 'Moq' version '4.18.4' added to file 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : Writing assets file to disk. Path: C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\project.assets.json
log  : Restored C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj (in 334 ms).


VS Code with UserServiceTests.cs:


Install VS Code test runner

The following steps are to install a test runner VS Code extension and a NuGet package for collecting code coverage data.

  1. Install the VS Code extension .NET Core Test Explorer, we'll be using this to run our tests in VS Code.
  2. In the /WebAPI.Tests project, install the Coverlet MSBuild package from NuGet with the following command:
    dotnet add package coverlet.msbuild
  3. Create or update the /.vscode/settings.json file with the following settings to configure the test explorer extension and coverlet:
    {
        "dotnet-test-explorer.autoExpandTree": true,
        "dotnet-test-explorer.autoWatch": true,
        "dotnet-test-explorer.treeMode": "full",
        "dotnet-test-explorer.testArguments": "/p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./TestResults/lcov.info",
        "dotnet-test-explorer.testProjectPath": "**/*.Tests.csproj"
    }
  4. Select the new VS Code Testing tab to view the three unit tests in our project, click the refresh icon if the tests aren't visible yet.


Command output and screenshots

VS Code after installing the .NET Core Test Explorer extension:


Output from dotnet add package coverlet.msbuild:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests> dotnet add package coverlet.msbuild
  Determining projects to restore...
  Writing C:\Users\Jason\AppData\Local\Temp\tmp691.tmp
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : X.509 certificate chain validation will use the default trust store selected by .NET.
info : Adding PackageReference for package 'coverlet.msbuild' into project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/coverlet.msbuild/index.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/coverlet.msbuild/index.json 1130ms
info : Restoring packages for C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj...
info : Package 'coverlet.msbuild' is compatible with all the specified frameworks in project 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : PackageReference for package 'coverlet.msbuild' version '3.2.0' added to file 'C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj'.
info : Generating MSBuild file C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\WebAPI.Tests.csproj.nuget.g.props.
info : Generating MSBuild file C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\WebAPI.Tests.csproj.nuget.g.targets.
info : Writing assets file to disk. Path: C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\obj\project.assets.json
log  : Restored C:\Projects\dotnet-7-dapper-sqlite-crud-api\WebAPI.Tests\WebAPI.Tests.csproj (in 377 ms).


VS Code after creating the /.vscode/settings.json file:


VS Code with the new Testing tab selected on the left menu:


Run tests and view code coverage

Now we'll run the tests and view the code coverage information a couple of different ways, first with the Coverage Gutters VS Code extension that highlights covered/uncovered lines within the VS Code editor pane, then we'll generate a code coverage HTML report for the whole project using the ReportGenerator dotnet tool.

  1. Run the tests by clicking the play button in the VS Code Testing tab. After they pass there should be a generated code coverage file at /WebAPI.Tests/TestResults/lcov.info, the file contains coverage data and is not meant to be directly human readable.
  2. Install the VS Code extension Coverage Gutters, it displays coverage info from the lcov file in the VS Code gutter to the left of the code.
  3. Open the WebAPI/Services/UserService.cs class and enable Coverage Gutters by clicking Watch in the VS Code status bar. The gutter next to the line numbers should be green for covered lines and red for uncovered lines, and the status bar should show the percentage that the file is covered.
  4. Finally let's generate an HTML coverage report for the whole project using the ReportGenerator dotnet tool. Install the ReportGenerator tool globally with the following command:
    dotnet tool install -g dotnet-reportgenerator-globaltool
  5. Create or update the /.vscode/tasks.json file with the following task to generate an HTML coverage report based on the lcov.info file:
    {
        // See https://go.microsoft.com/fwlink/?LinkId=733558
        // for the documentation about the tasks.json format
        "version": "2.0.0",
        "tasks": [
            {
                "label": "generate coverage report",
                "command": "reportgenerator",
                "type": "shell",
                "args": [
                    "-reports:${workspaceFolder}/WebApi.Tests/TestResults/lcov.info",
                    "-targetdir:${workspaceFolder}/WebApi.Tests/TestResults/coveragereport"
                ],
                "problemMatcher": []
            }
        ]
    }
  6. Run the task by selecting: Terminal > Run Task... > generate coverage report. Or with the shortcut: ctrl+shift+p > Tasks: Run Task > generate coverage report.
  7. In VS Code expand the folder WebAPI.Tests/TestResults/coveragereport, right click the index.htm file and select Show Preview. The coverage report will be displayed in the main panel.

That's it!

We're all setup to run unit tests and generate code coverage reports quickly and easily with VS Code for ASP.NET Core projects.


Command output and screenshots

The VS Code Testing tab after the tests are successfully run:


VS Code showing the generated lcov.info file with code coverage data, this file is used to generate coverage reports and is not intended to be directly human readable:


VS Code after installing the Coverage Gutters extension:


The UserService.cs class displaying coverage information with the coverage gutters extension:


Output from dotnet tool install -g dotnet-reportgenerator-globaltool:

PS C:\Projects\dotnet-7-dapper-sqlite-crud-api> dotnet tool install -g dotnet-reportgenerator-globaltool
You can invoke the tool using the following command: reportgenerator
Tool 'dotnet-reportgenerator-globaltool' (version '5.1.19') was successfully installed.


VS Code after creating the /.vscode/tasks.json file:


VS Code terminal output after running the report generator task:

 *  Executing task: reportgenerator -reports:C:\Projects\dotnet-7-dapper-sqlite-crud-api/WebApi.Tests/TestResults/lcov.info -targetdir:C:\Projects\dotnet-7-dapper-sqlite-crud-api/WebApi.Tests/TestResults/coveragereport 

2023-03-14T14:28:14: Arguments
2023-03-14T14:28:14:  -reports:C:\Projects\dotnet-7-dapper-sqlite-crud-api/WebApi.Tests/TestResults/lcov.info       
2023-03-14T14:28:14:  -targetdir:C:\Projects\dotnet-7-dapper-sqlite-crud-api/WebApi.Tests/TestResults/coveragereport
2023-03-14T14:28:14: Writing report file 'C:\Projects\dotnet-7-dapper-sqlite-crud-api/WebApi.Tests/TestResults/coveragereport\index.html'
 *  Terminal will be reused by tasks, press any key to close it.


Preview of generated HTML code coverage report in VS Code:

 


Need Some VS Code Help?

Search fiverr for freelance VS Code developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by