Skip to content

Advanced

Warning

xUnit call constructor before each test. Try to avoid unnecessary memory allocation inside constructor.

Unit test example🔗

Fact🔗

Facts - are tests which are always true. They test invariant conditions.

public class MyTests
{
    [Fact]
    public void Debug_OnInit_CalledOnce()
    {
        // arrange
        var loggerMock = new Mock<ILogger>();

        // act
        loggerMock.Object.Debug("second");

        // assert
        loggerMock.Verify(l => l.Debug(It.IsAny<string>()), Times.Once);
    }
}

Info

Read Fact article for more info.

Theory🔗

Theories - are tests which are only true for a particular set of data.

Inline Data🔗

Use InlineData attribute for constant expression objects

[Theory]
[InlineData(3)]
[InlineData(5)]
[InlineData(7)]
public void RemainderOfTheDivision_ForOddNumbers_ShouldBeOne(int value)
{
    // arrange & act
    var isOdd = value % 2 == 1;

    // assert
    isOdd.Should().BeTrue();
}

Class Data🔗

Use ClassData attribute for non-constant expression objects

public class SomeClass
{
    public int SomeValue { get; set; }
}

public class SomeClassData : TheoryData<SomeClass, bool>
{
    public SomeClassData()
    {
        // put here your tests cases
        Add(new SomeClass { SomeValue = 3 }, true);
        Add(new SomeClass { SomeValue = 5 }, true);
        Add(new SomeClass { SomeValue = 7 }, true);
        Add(new SomeClass { SomeValue = 6 }, false);
    }
}

[Theory]
[ClassData(typeof(SomeClassData))]
public void RemainderOfTheDivision_ForPassedNumbers_ShouldBeExpected(SomeClass someClass, bool result)
{
    // arrange & act
    var isOdd = someClass.SomeValue % 2 == 1;

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

Info

Read Theory article for more info.

Parallel test execution🔗

Keep in mind default xUnit behavior:

  1. Each test in same class should run synchronously.
  2. Different classes tests should run asynchronously.

Info

Read Running Tests in Parallel article for more info.

Shared context🔗

There are several approaches:

  1. Constructor and Dispose - shared setup/cleanup code without sharing object instances.
  2. Class Fixtures - shared object instance across tests in a single class with dispose.
  3. Static fields - shared object instance across tests in a single class without dispose.
  4. Collection Fixtures - shared object instances across multiple test classes.

Info

Read Shared Context article for more info.

Constructor and Dispose🔗

When to use: when you want a clean test context for every test (sharing the setup and cleanup code, without sharing the object instance).

You should read Constructor and Dispose article or follow retell below:

Retell

Use constructor for multi-line initialization before each test.

public class MyTests
{
    private static Mock<ILogger> _loggerMock = new Mock<ILogger>();

    public MyTests()
    {
        _loggerMock
            .Setup(l => l.CreateChildLogger(It.IsAny<string>()))
            .Returns(_loggerMock.Object);
    }
}

Use Dispose for cleanup after each test.

public class MyTests : IDisposable
{
    private static Mock<ILogger> _loggerMock = new Mock<ILogger>();

    public MyTests()
    {
        _loggerMock
            .Setup(l => l.CreateChildLogger(It.IsAny<string>()))
            .Returns(_loggerMock.Object);
    }

    public void Dispose()
    {
        _loggerMock.Invocations.Clear();
    }
}

In-Team conventions🔗

  1. Use field initializer for single-line initialization without setup before each test.

Do:

   public class MyTests
   {
       private Mock<ILogger> _loggerMock = new Mock<ILogger>();
   }
Don't:
   public class MyTests
   {
       private Mock<ILogger> _loggerMock;

       public MyTests()
       {
           _loggerMock = new Mock<ILogger>();
       }
   }

Class Fixtures🔗

When to use: when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished.

Note

xUnit, just before the first test, will create an instance of Fixture and pass the shared instance to the constructor.

You should read Class Fixtures article or follow retell below:

Retell

Follow steps below for sharing context.

  1. Create the fixture class, and put the startup code in the fixture class constructor.
    public class MyTestsFixture
    {
        public int SharedObject { get; }
    
        public MyTestsFixture()
        {
            // Setup test data
            SharedObject = 1;
        }
    }
    
  2. Implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
    public class MyTestsFixture : IDisposable
    {
        public IDisposable DisposableSharedObject { get; }
        public int SharedObject { get; }
    
        public MyTestsFixture()
        {
            // Setup test data
            SharedObject = 1;
        }
    
        public void Dispose()
        {
            // Cleanup test data
            DisposableSharedObject.Dispose();
        }
    }
    
  3. Add IClassFixture<> to the test class.
  4. If the test class needs access to the fixture instance, add it as a constructor argument, and it will be provided automatically.
    public class MyTests2 : IClassFixture<MyTestsFixture>
    {
        private readonly MyTestsFixture _fixture;
    
        public MyTests2(MyTestsFixture fixture)
        {
            _fixture = fixture;
        }
    
        [Fact]
        public void SharedObject_FromFixture_ShouldContainValue()
        {
            // arrange & act & assert
            _fixture.SharedObject.Should().Be(1);
        }
    }
    

In-Team conventions🔗

  1. Follow xUnit naming conventions - use fixture term.
  2. Use private readonly modificator - it shouldn't be possible to change this field outside of the constructor.
    private readonly MyTestsFixture _fixture;
    
  3. Store fixture class in a separate file, next to your test.
  4. Name of this file should be %TestsName%Fixture.cs.
    MyTests.cs
    MyTestsFixture.cs
    

Static fields🔗

When to use: when you want to create a single test context and share it among all the tests in the class.

public class MyTests : IDisposable
{
    private static Mock<ILogger> _loggerMock = new Mock<ILogger>();

    public void Dispose()
    {
        _loggerMock.Invocations.Clear();
    }

    [Fact]
    public void Debug_FirstCall_CalledOnce()
    {
        // arrange & act
        _loggerMock.Object.Debug("first");

        // assert
        _loggerMock.Verify(l => l.Debug(It.IsAny<string>()), Times.Once);
    }

    [Fact]
    public void Debug_SecondCall_InvocationCleared()
    {
        // arrange & act
        _loggerMock.Object.Debug("second");

        // assert
        _loggerMock.Verify(l => l.Debug(It.IsAny<string>()), Times.Once);
    }
}

Collection Fixtures🔗

When to use: when you want to create a single test context and share it among tests in several test classes, and have it cleaned up after all the tests in the test classes have finished.

You should read Collection Fixtures article or follow retell below:

Retell
  1. Create the fixture class, and put the startup code in the fixture class constructor.
  2. If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
  3. Create the collection definition class, decorating it with the [CollectionDefinition] attribute, giving it a unique name that will identify the test collection.
  4. Add ICollectionFixture<> to the collection definition class.
  5. Add the [Collection] attribute to all the test classes that will be part of the collection, using the unique name you provided to the test collection definition class's [CollectionDefinition] attribute.
  6. If the test classes need access to the fixture instance, add it as a constructor argument, and it will be provided automatically.
public class MyCollectionFixture
{
}

[CollectionDefinition(MyCollection.Name)]
public class MyCollection : ICollectionFixture<MyCollectionFixture>
{
    public const string Name = "Collection";
    // This class has no code, and is never created. Its purpose is simply
    // to be the place to apply [CollectionDefinition] and all the
    // ICollectionFixture<> interfaces.
}

[Collection(MyCollection.Name)]
public class Tests1
{
    private readonly MyCollectionFixture _collectionFixture;

    public Tests1(MyCollectionFixture collectionFixture)
    {
        _collectionFixture = collectionFixture;
    }
}

[Collection(MyCollection.Name)]
public class Tests2
{
    // ...
}

In-Team conventions🔗

  1. Try to avoid this type of context sharing because it rises test complexity and lowers readability.
  2. Follow DRY principle - add public const string as collection name.
    [CollectionDefinition(MyCollection.Name)]
    public class MyCollection : ICollectionFixture<MyCollectionFixture>
    {
        public const string Name = "Collection";
    }
    
  3. Follow xUnit naming conventions - use collectionFixture term.
    [Collection(MyCollection.Name)]
    public class Tests1
    {
        private readonly MyCollectionFixture _collectionFixture;
    
        public Tests1(MyCollectionFixture collectionFixture)
        {
            _collectionFixture = collectionFixture;
        }
    }
    

Base information🔗