Using xUnit Unit Testing

Using xUnit Unit Testing

Going back to basics: a unit test should verify a single bit of functionality (usually a method). I want tests to be isolated, so I do not call the internet or a real database. Tests are my safety net for "you just broke something".

I also like to test the unhappy paths. It is easy to only test the happy path, but the bugs always live in the corners.

Why xUnit?

It is the main testing tool used on the platform I work on, so I wanted to understand why and what the alternatives are.

Test attributes and organisation

Common testing frameworks are MSTest, NUnit, and xUnit. In xUnit we tag tests with [Fact] (single test) or [Theory] (data-driven test). MSTest and NUnit have similar attributes.

Test discovery

xUnit discovers tests via convention. It looks for those attributes on public classes inside a test project (usually ending in .Tests). One downside: discovery sometimes fails in IDEs. I have not had it fail personally, but I have seen it happen to QA.

Setup and teardown

xUnit uses the class constructor for setup and Dispose for teardown. NUnit/MSTest use [SetUp] and [TearDown]. The constructor approach feels more natural to me.

Test isolation

xUnit creates a new instance of the test class per test by default. NUnit and MSTest reuse the same instance unless you change it. The xUnit approach avoids strange side effects between tests.

Dependency injection support

xUnit plays nicely with DI because the constructor can take dependencies. You can pass in mocks and keep your tests close to how your production code is wired.

Parallel execution

By default, test collections run in parallel (tests within a class are sequential). This has been a nice speed boost for me.

Shared test context

If you have an expensive setup, you can share it using IClassFixture. I have seen this used for database-ish setup where you want to reuse a single instance.

Data-driven tests with Theory

I use Theory when I want the same test to run multiple times with different data.

[Fact]
public void Testy_Mc_Test_Face()

Using InlineData

[Theory]
[InlineData(1, 2, 3)]
public void Testy_Mc_Test_Face(int foo, int fee, int faa)

Using MemberData

public static IEnumerable<object[]> AddTestData => new List<object[]>
{
    new object[] { 0, 1, 1 },
    new object[] { 1, 2, 3 },
    new object[] { -1, 1, 0 }
};

[Theory]
[MemberData(nameof(AddTestData))]
public void Addition_Works(int a, int b, int expected)
{
    // Test logic here
}

Test naming

I still get this wrong sometimes, but the common convention is MethodName_WhenCondition_ExpectedOutcome.

So what is mocking?

Mocking is how we replace dependencies so tests stay isolated. That lets us control what happens when a method calls a database or a service. I actually enjoy mocking, but it can get tricky.

The mocking package I use at work is Moq. After installing it (dotnet add package Moq), you can create a mock like this:

var thingIMocked = new Mock<IThingIWantToMock>();

You can then control what the mock returns:

// For cases where you care about the specific value provided as the argument
thingIMocked.Setup(p => p.Add(100)).Returns(true);

// For cases where you do not care about the argument provided
thingIMocked.Setup(p => p.Add(It.IsAny<int>())).Returns(true);

This makes it easy to test different outcomes. You can also verify how many times a method was called.

Under the hood, Moq creates a fake class at runtime (using reflection and Dynamic Proxy) and intercepts method calls. It is clever, but it does have limits: it cannot mock static or sealed types and it cannot mock a class without a parameterless constructor. I have hit both of those.

Why I like using xUnit

  • Fresh test class instances for each test (no more weird interactions)
  • Constructor/Dispose feels like normal C#
  • DI and mocking support is solid
  • Parallel test execution by default
  • Theory tests make data-driven testing easy

Pick the testing tool that works for you. I like xUnit because I am familiar with it and it makes sense to me most of the time.

My takeaway: name your tests clearly. You will thank yourself later.

Happy testing.

Comments

No comments yet.

Add a comment