Top 5 useful unit test attributes in NUnit, MSTest and xUnit
Pavel Yermalovich

Pavel Yermalovich @pavel_yermalovich

About: Senior Software Engineer | Microsoft .NET / C# | Azure | AWS | Microservices | REST API | CI/CD | Product development

Location:
Copenhagen, Denmark
Joined:
Sep 2, 2024

Top 5 useful unit test attributes in NUnit, MSTest and xUnit

Publish Date: Jun 15
1 0

I’ve often seen one piece of advice repeated in software engineering books: 'Write a blog.'
So I finally carved out some time and wrote my very first blog post — and I chose a topic I care deeply about: unit testing.

In my experience, writing good unit tests (and other types of tests) is one of the most effective ways to catch bugs before code reaches production. It becomes especially important when your team doesn’t have a dedicated QA engineer — your tests are your safety net.

Over the years, I’ve found and fixed many issues in production systems where the root cause was simple: the change wasn’t covered by a unit test. In those cases, my first step was almost always the same — write the missing test that would fail, and then add the fix.

One of the best tools for making tests effective, readable, and maintainable is understanding how to use test framework attributes. These attributes control setup, teardown, input data, and test behavior — and when used well, they make your test code more expressive and powerful.

Unit test attributes help write cleaner, smarter, and more scalable tests — and knowing how they work across frameworks like NUnit, MSTest, and xUnit provides long-term flexibility.

This post shows top 5 most useful (in my experience) NUnit attributes. Each example also provides the MSTest and xUnit equivalents.


1. SetUp attribute

Purpose

Marks a method to be run before each test method in a test fixture. This is typically used to initialize objects, mocks, or shared state needed for every test.

private OrderService _orderService;

private Mock<IInventoryService> _inventoryMock;
private Mock<ILogger> _loggerMock;
private Mock<IOrderRepository> _repoMock;

[SetUp]
public void SetUp()
{
    _inventoryMock = new Mock<IInventoryService>(MockBehavior.Strict);
    _loggerMock = new Mock<ILogger>(MockBehavior.Strict);
    _repoMock = new Mock<IOrderRepository>(MockBehavior.Strict);

    _orderService = new OrderService(
      _inventoryMock.Object, 
      _loggerMock.Object, 
      _repoMock.Object);
}

[TestMethod]
public async Task PlaceOrderAsync_ProductInStock_SavesOrderAndLogsSuccess()
{
    // Arrange
    var productId = "A123";
    var quantity = 2;

    _inventory.Setup(x => x.IsInStockAsync(productId, quantity)).ReturnsAsync(true);
    _orderRepository.Setup(x => x.SaveOrderAsync(productId, quantity));
    _logger.Setup(x => x.LogAsync($"Order placed for {quantity} of {productId}."));

    // Act
    var result = await _orderService.PlaceOrderAsync(productId, quantity);

    // Assert
    Assert.That(result, Is.True);
}
Enter fullscreen mode Exit fullscreen mode

Behavior

Runs before every [Test], [TestCase], or [TestCaseSource] method in the class.

If a [SetUp] method throws an exception, the test is skipped and reported as failed or errored.

Advanced Usage

You can define more than one [SetUp] method, but they are typically used across different levels in an inheritance hierarchy.

[SetUp] methods may be either instance or static methods, depending on the test design.

Why Use It

Centralizing setup logic avoids duplication and ensures each test starts from a consistent state. This is particularly useful when working with mocks or test doubles that need to be freshly configured per test case.

Framework Equivalents

  • MSTest: [TestInitialize]
  • xUnit: Use the class constructor for per-test setup

2. TestCase attribute

Purpose

Defines a parameterized test that runs multiple times with different sets of arguments. Each [TestCase] represents a separate invocation of the test method.

[TestCase(2, 3, 5)]
[TestCase(0, 0, 0)]
[TestCase(-1, -1, -2)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
    var result = Calculator.Add(a, b);
    Assert.That(result, Is.EqualTo(expected));
}

[TestCase("hello", "Hello")]
[TestCase("test", "Test")]
[TestCase("Test", "Test")]
[TestCase("", "")]
[TestCase(" ", " ")]
public void CapitalizeFirstLetter_WorksAsExpected(string input, string expected)
{
     var result = input.CapitalizeFirstLetter();
     Assert.That(result, Is.EqualTo(expected));
}
Enter fullscreen mode Exit fullscreen mode

Behavior

Each [TestCase] line executes the test method with its own arguments.

NUnit displays each case separately in the test runner, making it easy to identify failing inputs.

Why Use It

Helps reduce duplication when testing the same logic with multiple input combinations. It’s particularly useful for pure functions like Add(a, b) or input validation scenarios.

Advanced Usage

Supports optional TestName for custom naming in test runners.
Can combine with [TestCaseSource] if inline values become unwieldy.

Framework Equivalents

  • MSTest: [DataTestMethod] with [DataRow(...)]

  • xUnit: [Theory] with [InlineData(...)]


3. TestCaseSource attribute

Purpose

Provides test data from an external source such as a method, property, or field. This is useful when test cases are too complex or numerous to define inline with [TestCase].

public static IEnumerable<TestCaseData> OrderScenarios()
{
    yield return new TestCaseData("USB-C Cable", 1, true, "Order placed for 1 of USB-C Cable.")
        .SetName("Order succeeds when USB-C Cable is in stock");

    yield return new TestCaseData("Wireless Mouse", 2, false, "Product Wireless Mouse is out of stock.")
        .SetName("Order fails when Wireless Mouse is out of stock");

    yield return new TestCaseData("Laptop Stand", 0, false, "Invalid quantity: 0 for product Laptop Stand.")
        .SetName("Order fails with zero quantity");
}

[TestCaseSource(nameof(OrderScenarios))]
public async Task PlaceOrderAsync_ReturnsExpectedResult_AndLogsCorrectly(
    string productName, int quantity, bool inStock, string expectedLog)
{
    // Arrange
    _inventoryMock.Setup(x => x.IsInStockAsync(productName, quantity))
        .ReturnsAsync(inStock)
        .Verifiable();

    if (inStock && quantity > 0)
    {
        _orderRepoMock.Setup(x => x.SaveOrderAsync(productName, quantity))
            .Returns(Task.CompletedTask)
            .Verifiable();
    }

    _loggerMock.Setup(x => x.LogAsync(expectedLog))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await _orderService.PlaceOrderAsync(productName, quantity);

    // Arrange
    Assert.That(result, Is.EqualTo(inStock && quantity > 0));
}
Enter fullscreen mode Exit fullscreen mode

Behavior

  • Loads test data at runtime.

  • Each element in the data source results in one invocation of the test method.

  • The source must return IEnumerable, and each item must match the method signature.

Why Use It

Ideal for tests requiring:

  • Reusable datasets
  • Long or structured data
  • Programmatically generated input

Advanced Usage

Can return TestCaseData to include named cases and expected results.
Can be combined with external files, constants, or shared test data objects.

Framework Equivalents

  • MSTest: [DynamicData]
  • xUnit: [MemberData]

4. TearDown attribute

Purpose

Marks a method to be executed after each test method runs. It’s commonly used for cleanup or verification — for example, to check that all expected interactions with mocks occurred.

private OrderService _orderService;

private Mock<IInventoryService> _inventoryMock;
private Mock<ILogger> _loggerMock;
private Mock<IOrderRepository> _repoMock;

// ...

[TearDown]
public void TearDown()
{
    _inventoryMock.VerifyAll();
    _repoMock.VerifyAll();
    _loggerMock.VerifyAll();
}
Enter fullscreen mode Exit fullscreen mode

Behavior

Runs after every [Test], [TestCase], or [TestCaseSource] method.

If a [TearDown] method throws an exception, NUnit reports it as an error after the test result.

Why Use It

Perfect for verifying mocks, releasing resources, or resetting state shared across tests. It helps you avoid repetitive assertions or cleanup logic in each test method.

Advanced Usage

You can have multiple [TearDown] methods through class inheritance.

You can perform conditional checks or assertions on test outcomes by capturing test context (e.g., with TestContext.CurrentContext).

Framework Equivalents

  • MSTest: [TestCleanup]

  • xUnit: IDisposable.Dispose() or IAsyncLifetime.DisposeAsync()


5. OneTimeSetUp attribute

Purpose

Marks a method to be executed once before all tests in a test fixture. It’s typically used for expensive setup that doesn’t need to be repeated before each test — for example, initializing shared resources, seeding in-memory databases, or loading static configuration.

Example 1: Setup that doesn’t need to be repeated before each test

[OneTimeSetUp]
public void OneTimeSetup()
{
    // Simulated tax configuration loaded once for all tests
    _taxRates = new Dictionary<decimal, decimal>
    {
        { 50m, 12.5m },
        { 100m, 25m },
        { 150m, 37.5m },
        { 200m, 0m },
        { 300m, 60m }
    };
}
Enter fullscreen mode Exit fullscreen mode

Example 2: Seeding an in-memory DbContext
Suppose you're testing a CustomerRepository that reads from a database:


[TestFixture]
public class CustomerRepositoryTests
{
    private static AppDbContext _dbContext;
    private CustomerRepository _repository;

    [OneTimeSetUp]
    public void SetupDatabase()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase("TestDb")
            .Options;

        _dbContext = new AppDbContext(options);

        _dbContext.Customers.AddRange(
            new Customer { Id = 1, Name = "Alice" },
            new Customer { Id = 2, Name = "Bob" }
        );

        _dbContext.SaveChanges();
    }

    [SetUp]
    public void Init()
    {
        _repository = new CustomerRepository(_dbContext);
    }

    [TestCase(1, "Alice")]
    [TestCase(2, "Bob")]
    public void GetCustomerById_ReturnsExpectedName(int id, string expectedName)
    {
        var result = _repository.GetCustomerById(id);
        Assert.That(result?.Name, Is.EqualTo(expectedName));
    }
}

Enter fullscreen mode Exit fullscreen mode

Behavior

Runs once before any [Test], [TestCase], or [TestCaseSource] in the fixture.

If it throws an exception, all tests in the class are skipped or marked as failed.

The method can be instance or static.

Why Use It

Improves performance by avoiding repeated setup

Keeps your tests focused on logic, not setup

Useful for read-only test scenarios (e.g., queries, lookups, projections)

Advanced Usage

Can be combined with [OneTimeTearDown] to dispose of resources like DbContext, test servers, or file handles

If your tests mutate state, consider moving setup to [SetUp] instead to avoid test interdependence

Framework Equivalents

  • MSTest: [ClassInitialize]
  • xUnit: IClassFixture<T> or IAsyncLifetime.InitializeAsync()

Thanks for taking the time to read this post — I hope it helped clarify how test attributes can improve your unit testing practice across NUnit, MSTest, and xUnit.


Now I’d love to hear from you:

  • Which test attributes do you use most often in your projects?

  • Are there any underrated attributes you think more developers should know about?

Feel free to share your experience, opinions, or favorite testing tricks in the comments. Let’s make unit tests a little less painful — and a lot more useful — together.

Comments 0 total

    Add comment