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);
}
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));
}
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));
}
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();
}
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()
orIAsyncLifetime.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 }
};
}
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));
}
}
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>
orIAsyncLifetime.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.