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:
- Each test in same class should run synchronously.
- Different classes tests should run asynchronously.
Info
Read Running Tests in Parallel article for more info.
Shared context🔗
There are several approaches:
- Constructor and Dispose - shared setup/cleanup code without sharing object instances.
- Class Fixtures - shared object instance across tests in a single class with dispose.
- Static fields - shared object instance across tests in a single class without dispose.
- 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.
In-Team conventions🔗
- Use field initializer for single-line initialization without setup before each test.
Do:
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.
- Create the fixture class, and put the startup code in the fixture class constructor.
- Implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
- Add IClassFixture<> to the test class.
- If the test class needs access to the fixture instance, add it as a constructor argument, and it will be provided automatically.
In-Team conventions🔗
- Follow xUnit naming conventions - use
fixtureterm. - Use
private readonlymodificator - it shouldn't be possible to change this field outside of the constructor. - Store fixture class in a separate file, next to your test.
- Name of this file should be %TestsName%Fixture.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
- Create the fixture class, and put the startup code in the fixture class constructor.
- If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
- Create the collection definition class, decorating it with the
[CollectionDefinition]attribute, giving it a unique name that will identify the test collection. - Add ICollectionFixture<> to the collection definition class.
- 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. - 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🔗
- Try to avoid this type of context sharing because it rises test complexity and lowers readability.
- Follow DRY principle - add
public const stringas collection name. - Follow xUnit naming conventions - use
collectionFixtureterm.