English
Français

Blog of Denis VOITURON

for a better .NET world

Unit Tests and Code Coverage

Overview

Automated testing is a great way to ensure that the application code does what its developers want it to do.

There are different types of tests that can be used to validate the code. 3 of them are:

  1. Unit tests

    A unit test is a test that exercises individual software components or methods, also known as “unit of work”. Unit tests should only test code within the developer’s control. They do not test infrastructure concerns. Infrastructure concerns include interacting with databases, file systems, and network resources.

  2. Integration tests

    An integration test differs from a unit test in that it exercises two or more software components’ ability to function together, also known as their “integration.” These tests operate on a broader spectrum of the system under test, whereas unit tests focus on individual components. Often, integration tests do include infrastructure concerns.

  3. Load tests

    A load test aims to determine whether or not a system can handle a specified load, for example, the number of concurrent users using an application and the app’s ability to handle interactions responsively.




Why unit test?


Copied from Unit testing Best Practice.




Seven Best practices

See detailed best practices.

  1. Naming your tests

    The name of your test should consist of three parts:

    • The name of the method being tested.
    • The scenario under which it’s being tested.
    • The expected behavior when the scenario is invoked.

    Why?

    • Naming standards are important because they explicitly express the intent of the test.

    Example:

    [Fact]
    public void Add_SingleNumber_ReturnsSameNumber()
    


  2. Arranging your tests

    Arrange, Act, Assert is a common pattern when unit testing. As the name implies, it consists of three main actions:

    • Arrange your objects, creating and setting them up as necessary.
    • Act on an object.
    • Assert that something is as expected.

    Why?

    • Clearly separates what is being tested from the arrange and assert steps.
    • Less chance to intermix assertions with “Act” code.

    Example:

    [Fact]
    public void Add_EmptyString_ReturnsZero()
    {
       // Arrange
       var stringCalculator = new StringCalculator();
    
       // Act
       var actual = stringCalculator.Add("");
    
       // Assert
       Assert.Equal(0, actual);
    }
    


  3. Write minimally passing tests

    The input to be used in a unit test should be the simplest possible in order to verify the behavior that you are currently testing.

    Why?

    • Tests become more resilient to future changes in the codebase.
    • Closer to testing behavior over implementation.


  4. Avoid logic in tests

    When writing your unit tests avoid manual string concatenation and logical conditions such as if, while, for, switch, etc.

    Why?

    • Less chance to introduce a bug inside of your tests.
    • Focus on the end result, rather than implementation details.

    Example:

    [Theory]
    [InlineData("0,0,0", 0)]
    [InlineData("0,1,2", 3)]
    [InlineData("1,2,3", 6)]
    public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
    {
       var stringCalculator = new StringCalculator();
    
       var actual = stringCalculator.Add(input);
    
       Assert.Equal(expected, actual);
    }
    


  5. Prefer helper methods to setup and teardown

    If you require a similar object or state for your tests, prefer a helper method than leveraging constructor or Setup and Teardown attributes if they exist.

    Why?

    • Less confusion when reading the tests since all of the code is visible from within each test.
    • Less chance of setting up too much or too little for the given test.
    • Less chance of sharing state between tests, which creates unwanted dependencies between them.

    Example:

    // Bad
    // public StringCalculatorTests()
    // {
    //    stringCalculator = new StringCalculator();
    // }
    
    [Fact]
    public void Add_TwoNumbers_ReturnsSumOfNumbers()
    {
       var stringCalculator = CreateDefaultStringCalculator();
    
       var actual = stringCalculator.Add("0,1");
    
       Assert.Equal(1, actual);
    }
    
    private StringCalculator CreateDefaultStringCalculator()
    {
       return new StringCalculator();
    }
    


  6. Avoid multiple acts

    When writing your tests, try to only include one Act per test. Common approaches to using only one act include:

    Create a separate test for each act or use parameterized tests.

    Why?

    • When the test fails it is not clear which Act is failing.
    • Ensures the test is focussed on just a single case.
    • Gives you the entire picture as to why your tests are failing.


  7. How to mock DateTime?

    One of the principles of a unit test is that it must have full control of the system under test. This can be problematic when production code includes calls to static references like DateTime.Now.

    To solve that, use this DateTimeProvider in your production code and you will write your unit test like this:

    public void CurrentDate_Today_ReturnsDay()
    {
        var MOCK_TODAY = new DateTime(2018, 5, 26);
        using (var context = new DateTimeProviderContext(MOCK_TODAY))
        {
            var now = DateTimeProvider.Now;
            Assert.Equal(26, now.Day);
        }
    }
    

    With this DateTimeProvider.

     public class DateTimeProvider
     {
         public static DateTime Now
             => DateTimeProviderContext.Current == null
                     ? DateTime.Now
                     : DateTimeProviderContext.Current.ContextDateTimeNow;
    
         public static DateTime UtcNow => Now.ToUniversalTime();
    
         public static DateTime Today => Now.Date;
     }
    
     public class DateTimeProviderContext : IDisposable
     {
         internal DateTime ContextDateTimeNow;
         private static ThreadLocal<Stack> ThreadScopeStack = new ThreadLocal<Stack>(() => new Stack());
         private Stack _contextStack = new Stack();
    
         public DateTimeProviderContext(DateTime contextDateTimeNow)
         {
             ContextDateTimeNow = contextDateTimeNow;
             ThreadScopeStack.Value.Push(this);
         }
    
         public static DateTimeProviderContext Current
         {
             get
             {
                 if (ThreadScopeStack.Value.Count == 0)
                     return null;
                 else
                     return ThreadScopeStack.Value.Peek() as DateTimeProviderContext;
             }
         }
    
         public void Dispose()
         {
             ThreadScopeStack.Value.Pop();
         }
     }
    

    More details on this page.



Code Coverage

Unit tests help to ensure functionality and provide a means of verification for refactoring efforts. Code coverage is a measurement of the amount of code that is run by unit tests - either lines, branches, or methods.

This chapter discusses the usage of code coverage for unit testing with Coverlet and report generation using ReportGenerator.

  1. Requirements

    Include the NuGet Packages coverlet.msbuild and coverlet.collector in your Unit Tests Project (csproj).

    <PackageReference Include="coverlet.msbuild" Version="3.1.2" />
    <PackageReference Include="coverlet.collector" Version="3.1.2" />
    
  2. Tools

    To generate a code coverage report locally, install these tools: Coverlet and ReportGenerator.

    dotnet tool install --global coverlet.console --version 3.1.2
    dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.1.10
    

    Use this command to list and verify existing installed tools:

    dotnet tool list --global
    
  3. Start a code coverage

    You can start the unit test and code coverage tool using this command (in the solution folder). Each unit test project folders will contain a file coverage.cobertura.xml.

    dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
    
  4. Generate a report

    Merge and convert all Cobertura.xml files to an HTML report (change the sample folder Temp/Coverage).

    reportgenerator "-reports:**/*.cobertura.xml" "-targetdir:C:\Temp\Coverage" -reporttypes:HtmlInline_AzurePipelines
    



You can copy/paste this content in a file _StartCodeCoverage.cmd to generate and display the code coverage report.

echo off

REM 0. Include the NuGet Packages "coverlet.msbuild" and "coverlet.collector" in the UnitTests project.
REM 1. Install tools:
REM     $:\> dotnet tool install --global coverlet.console --version 3.1.2
REM     $:\> dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.1.10
REM    
REM     Use this command to list existing installed tools:
REM     $:\> dotnet tool list --global
REM
REM 2. Start a code coverage in the UnitTests project:
REM     $:\> dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
REM 
REM 3. Display the Coverage Report:
REM     $:\> reportgenerator "-reports:coverage.cobertura.xml" "-targetdir:C:\Temp\Coverage" -reporttypes:HtmlInline_AzurePipelines
REM     $:\> explorer C:\Temp\Coverage\index.html

echo on
cls

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
reportgenerator "-reports:coverage.cobertura.xml" "-targetdir:C:\Temp\Coverage" -reporttypes:HtmlInline_AzurePipelines
start "" "C:\Temp\Coverage\index.htm"

More details on this page.



Blazor Unit Tests

All Blazor unit tests use the bUnit and Verifier.Blazor libraries. Its objective is to facilitate the writing of complete and stable unit tests.

With bUnit, you can:

bUnit builds on top of existing unit testing frameworks such as xUnit, NUnit, and MSTest

  1. Example with a simple parameter:
    [Fact]
    public void MyButton_Basic_Width()
    {
        // Arrange
        using var ctx = new Bunit.TestContext();
    
        // Act
        var button = ctx.RenderComponent<MyButton>(parameters =>
        {
            parameters.Add(p => p.Width, "100px")
        });
    
        // Assert
        button.MarkupMatches(@"<fluent-button appearance=""neutral"" style=""width: 100px;"" />");
    }
    
  2. Example using the Verifier library:
    [UsesVerify]
    public class MyToolbarTests
    {
        [Fact]
        public Task MyToolbar_Render_TwoButtons()
        {
            // Arrange
            using var ctx = new Bunit.TestContext();
    
            // Act
            var toolbar = ctx.RenderComponent<MyToolbar>(parameters =>
            {
                parameters.AddChildContent<MyButton>(item =>
                {
                    item.AddChildContent("Button 1");
                });
                parameters.AddChildContent<MyButton>(item =>
                {
                    item.AddChildContent("Button 2");
                });
            });
        
            // Assert
            return Verifier.Verify(toolbar);
        }
    }
    

    This method will generate a file .received.html** which will be compared to **.verified.html.

    <!-- MyToolbar_Render_TwoButtons.verified.html -->
    <div class="stack-horizontal">
        <div class="my-toolbar">
            <fluent-button appearance="neutral">Button 1</fluent-button>
            <fluent-button appearance="neutral">Button 2</fluent-button>
        </div>
    </div>
    

⚠️ Using Verifier, when a test fails, Visual Studio automatically opens the Diff Compare Tool. You can disable this feature adding the Environment variable DiffEngine_Disabled=True.

Languages

EnglishEnglish
FrenchFrançais

Follow me

Recent posts