Skip to content

Basics

This is in-team conventions about unit testing for all backend services.

Main libraries🔗

  • xUnit - Framework for unit testing
  • Moq - Isolation framework
  • FluentAssertions - Extension methods that allow you to more naturally specify the expected outcome

Unit Test basic structure🔗

Use plural postfix for test class name %ClassName%Tests

public class ClassNameTests
{        
    [Fact]
    public void MethodName_StateUnderTest_ExpectedBehavior()
    {
        // test body
    }
}

Info

Additional scenarios described in advanced section.

Characteristics of a good unit test🔗

  • Fast. It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run (milliseconds).
  • Isolated. Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • Repeatable. Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs.
  • Self-Checking. The test should be able to automatically detect if it passed or failed without any human interaction.
  • Timely. A unit test should not take a disproportionately long time to write compared to the code being tested. If you find testing the code taking a large amount of time compared to writing the code, consider a design that is more testable.

Naming convention🔗

Naming standards are important because they explicitly express the intent of the test. Always follow Microsoft naming conventions. The name of your test should consist of three parts:

  1. MethodName - The name of the method being tested.
  2. StateUnderTest - The scenario under which it's being tested.
  3. ExpectedBehavior - The expected behavior when the scenario is invoked.
public void MethodName_StateUnderTest_ExpectedBehavior()
{
}

Info

Read Unit Test Naming Conventions article for more info.

Arrange-Act-Assert🔗

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

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

Always split you test on 3 sections.

public class MyTests
{        
    [Fact]
    public void MethodName_StateUnderTest_ExpectedBehavior()
    {
        // arrange

        // act

        // assert
    }
}
If a single line of code represents two actions, write both of them with &
public class MyTests
{        
    [Fact]
    public void MethodName_StateUnderTest_ExpectedBehavior()
    {
        // arrange & act
        var result = 1 + 1;

        // assert
        result.Should().Be(2);
    }
}

Info

Read Arranging Conventions article for more info.

Cover convention🔗

When you try to discover what logic should be covered follow these rules:

  1. Cover uncommon logic.
  2. Avoid cover common logic.

What does this mean?

Common logic🔗

Common logic - logic provided by external sources.

This should NOT be covered:

  • Methods provided external libraries, like:
    • Archive() method of Zip library
    • Send() method of ServiceBus library
    • etc.
  • DataBase logic:
  • .NET logic like getters, setters, constructor arguments, etc.
  • DTO - database entities, records and classes without logic
  • API Controller methods - because there are should not be any logic

Uncommon logic🔗

Uncommon logic - logic written by us.

This SHOULD be covered:

  • Public methods of implementations
  • Business logic:
    • if-else cases
    • do-while sections
    • try-catch sections
    • switch expressions
    • etc.
  • Compile-time logic of external libraries. If it provides validation methods. Like:
  • Regular expressions
  • Any discovered bugs should be covered by a unit test in the first place

Service Provider Unit Testing🔗

Each project should contain Service Provider Unit tests. Without it, we can get runtime errors. Example:

[Fact]
public void BuildServiceProvider_WithDependencies_ShouldNotThrow()
{
    // arrange
    var services = new ServiceCollection();
    var serviceProviderOptions = new ServiceProviderOptions
    {
        ValidateOnBuild = true,
        ValidateScopes = true,
    };

    services.AddYourProjectServices() // add your dependencies here

    // act
    var buildServiceCollection = () =>
    {
        var serviceProvider = services.BuildServiceProvider(serviceProviderOptions);
        serviceProvider.CreateScope();
    };

    // assert
    buildServiceCollection.Should().NotThrow();
}

Mapster Unit Testing🔗

Each project with Mapster should contain its configuration unit tests. Without it, we can get runtime errors. Example:

[Fact]
public void Compile_WithConfigurations_ShouldNotThrow()
{
    // Arrange
    var config = new TypeAdapterConfig
    {
        RequireExplicitMapping = true,
        RequireDestinationMemberSource = true
    };

    config.Scan(Assembly.GetAssembly(typeof(Program))!); // add your configurations

    // Act
    Action compileAction = () => config.Compile();

    // Assert
    compileAction.Should().NotThrow();
}

GitlabCI (IN PROGRESS)🔗

How we should count code coverage? How we should write reports? Cobertura - tool that calculates the percentage of code accessed by tests. coverlet

FAQ (IN PROGRESS)🔗

Any questions.

Base information🔗