Overview
Unit testing is fundamental to building maintainable, reliable software. Understanding xUnit thoroughly is essential for .NET developers at all levels, as it's commonly used in production applications and frequently discussed in technical interviews.
Core Concepts
Fact vs Theory
// Fact - single test case
[Fact]
public void IsEven_EvenNumber_ReturnsTrue()
{
var result = MathHelper.IsEven(4);
Assert.True(result);
}
// Theory - multiple test cases with data
[Theory]
[InlineData(2)]
[InlineData(4)]
[InlineData(100)]
public void IsEven_EvenNumbers_ReturnsTrue(int number)
{
var result = MathHelper.IsEven(number);
Assert.True(result);
}
[Theory]
[InlineData(1, false)]
[InlineData(2, true)]
[InlineData(3, false)]
[InlineData(4, true)]
public void IsEven_VariousNumbers_ReturnsExpectedResult(int number, bool expected)
{
var result = MathHelper.IsEven(number);
Assert.Equal(expected, result);
}
AAA Pattern (Arrange-Act-Assert)
public class UserServiceTests
{
[Fact]
public void CreateUser_ValidData_ReturnsUserId()
{
// Arrange - setup test data and dependencies
var service = new UserService();
var request = new CreateUserRequest
{
Name = "John Doe",
Email = "john@example.com"
};
// Act - execute the method under test
var userId = service.CreateUser(request);
// Assert - verify the expected outcome
Assert.True(userId > 0);
}
}
Test Naming Conventions
// Pattern: MethodName_Scenario_ExpectedBehavior
[Fact]
public void GetUser_ExistingId_ReturnsUser() { }
[Fact]
public void GetUser_NonExistingId_ReturnsNull() { }
[Fact]
public void SaveUser_NullUser_ThrowsArgumentNullException() { }
[Fact]
public void ValidateEmail_InvalidFormat_ReturnsFalse() { }
Common Assertions
public class AssertionExamples
{
[Fact]
public void DemonstrateAssertions()
{
// Equality
Assert.Equal(5, 2 + 3);
Assert.NotEqual(5, 2 + 2);
// Boolean
Assert.True(5 > 3);
Assert.False(5 < 3);
// Null checks
Assert.NotNull(new object());
Assert.Null(null);
// Collections
var list = new List<int> { 1, 2, 3 };
Assert.Contains(2, list);
Assert.DoesNotContain(5, list);
Assert.Empty(new List<int>());
Assert.NotEmpty(list);
Assert.Equal(3, list.Count);
// Exceptions
Assert.Throws<ArgumentNullException>(() => throw new ArgumentNullException());
// Types
Assert.IsType<string>("hello");
Assert.IsAssignableFrom<IEnumerable<int>>(list);
// Ranges (for floating point)
Assert.Equal(3.14, 3.1415, precision: 2);
}
}
Test Data from External Sources
// MemberData - from static method/property
public class CalculatorTests
{
public static IEnumerable<object[]> AddTestData =>
new List<object[]>
{
new object[] { 1, 2, 3 },
new object[] { -1, 1, 0 },
new object[] { 100, 200, 300 }
};
[Theory]
[MemberData(nameof(AddTestData))]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
// ClassData - from separate class
public class AddTestDataProvider : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { 1, 2, 3 };
yield return new object[] { -1, 1, 0 };
yield return new object[] { 100, 200, 300 };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
[Theory]
[ClassData(typeof(AddTestDataProvider))]
public void Add_ClassData_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
Bad vs Good Examples
Bad: Testing Multiple Concerns
// Bad - tests too much at once
[Fact]
public void ProcessOrder_ValidOrder_Works()
{
var service = new OrderService();
var order = new Order { Items = new[] { new Item { Price = 10 } } };
var result = service.ProcessOrder(order);
// Too many assertions - what exactly are we testing?
Assert.NotNull(result);
Assert.True(result.Id > 0);
Assert.Equal(10, result.Total);
Assert.Equal("Processed", result.Status);
Assert.NotNull(result.ProcessedDate);
Assert.True(result.ProcessedDate <= DateTime.UtcNow);
}
Good: Single Responsibility Tests
// Good - each test verifies one behavior
[Fact]
public void ProcessOrder_ValidOrder_AssignsId()
{
var service = new OrderService();
var order = new Order { Items = new[] { new Item { Price = 10 } } };
var result = service.ProcessOrder(order);
Assert.True(result.Id > 0);
}
[Fact]
public void ProcessOrder_ValidOrder_CalculatesCorrectTotal()
{
var service = new OrderService();
var order = new Order { Items = new[] { new Item { Price = 10 }, new Item { Price = 20 } } };
var result = service.ProcessOrder(order);
Assert.Equal(30, result.Total);
}
[Fact]
public void ProcessOrder_ValidOrder_SetsStatusToProcessed()
{
var service = new OrderService();
var order = new Order { Items = new[] { new Item { Price = 10 } } };
var result = service.ProcessOrder(order);
Assert.Equal("Processed", result.Status);
}
Bad: Hard-Coded Dependencies
// Bad - tightly coupled to database
public class BadUserServiceTests
{
[Fact]
public void GetUser_ExistingUser_ReturnsUser()
{
// Bad - requires real database
var service = new UserService(new SqlConnection("connectionString"));
var user = service.GetUser(1);
Assert.NotNull(user);
}
}
Good: Dependency Injection
// Good - dependencies injected, can be mocked
public interface IUserRepository
{
User GetById(int id);
}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
public User GetUser(int id) => _repository.GetById(id);
}
public class GoodUserServiceTests
{
[Fact]
public void GetUser_ExistingUser_ReturnsUser()
{
// Arrange - use test double (covered in mocking lesson)
var repository = new FakeUserRepository();
repository.Users.Add(new User { Id = 1, Name = "Test" });
var service = new UserService(repository);
// Act
var user = service.GetUser(1);
// Assert
Assert.NotNull(user);
Assert.Equal("Test", user.Name);
}
}
// Simple test double for demonstration
public class FakeUserRepository : IUserRepository
{
public List<User> Users { get; } = new();
public User GetById(int id) => Users.FirstOrDefault(u => u.Id == id);
}
Interview Tips
Tip 1: Emphasize the AAA pattern - it makes tests readable and maintainable. Interviewers appreciate structured thinking.
Tip 2: Know when to use
[Fact]vs[Theory]- Facts for single cases, Theories for parameterized tests with multiple inputs.
Tip 3: Understand test naming conventions - clear names document behavior and make failures easier to understand.
Common Interview Questions
-
What's the difference between [Fact] and [Theory] in xUnit?
[Fact]is for a single test case with no parameters.[Theory]is for parameterized tests with data from[InlineData],[MemberData], or[ClassData].
-
What is the AAA pattern in unit testing?
- Arrange (setup test data), Act (execute method under test), Assert (verify results). Provides clear test structure and readability.
-
How do you test for exceptions in xUnit?
- Use
Assert.Throws<TException>(() => /* code that throws */). It verifies the exception type is thrown and returns the exception for further assertions.
- Use
-
What makes a good unit test?
- Fast, isolated, repeatable, self-validating, timely (FIRST). Also: single responsibility, descriptive name, independent of execution order.
-
How do you handle test data in xUnit?
[InlineData]for simple values,[MemberData]for data from methods/properties,[ClassData]for complex data from separate classes.
-
What's the difference between Assert.Equal and Assert.Same?
Equalchecks value equality (uses.Equals()).Samechecks reference equality (same object instance).
-
Why should unit tests be isolated?
- Isolated tests are independent, don't share state, can run in any order or parallel. Makes tests reliable and easier to debug.