Unit testing is the practice of testing individual units or components of code in isolation, typically at the function or method level. A unit test verifies that a specific piece of code behaves as expected under various conditions.
Why it's important:
- Early Bug Detection: Catches bugs early in development when they're cheaper to fix
- Documentation: Tests serve as living documentation showing how code should be used
- Refactoring Confidence: Enables safe refactoring by ensuring existing functionality isn't broken
- Design Improvement: Writing testable code often leads to better architecture and loose coupling
- Regression Prevention: Prevents old bugs from reappearing
- Faster Development: Though initial setup takes time, it speeds up long-term development
- Quality Assurance: Provides confidence that code works correctly
Related Resources
The AAA pattern is a common structure for organizing unit tests, making them clear and consistent:
1. Arrange: Set up the test conditions
- Initialize objects
- Configure dependencies
- Set up test data and expected values
2. Act: Execute the code under test
- Call the method or function being tested
- Trigger the behavior you want to verify
3. Assert: Verify the results
- Check that the actual outcome matches expectations
- Verify state changes, return values, or method calls
Example:
[Fact]
public void Withdraw_WithSufficientFunds_DecreasesBalance()
{
// Arrange
var account = new BankAccount(initialBalance: 100);
var withdrawAmount = 30;
// Act
account.Withdraw(withdrawAmount);
// Assert
Assert.Equal(70, account.Balance);
}
Related Resources
These are different types of test doubles used to isolate code during testing:
Stubbing:
- Provides predefined answers to method calls
- Used when you need to control what dependencies return
- Doesn't verify interactions
- Simple, passive replacement
var userRepository = new Mock();
userRepository.Setup(x => x.GetById(1)).Returns(new User { Id = 1, Name = "John" });
Mocking:
- Records and verifies interactions with dependencies
- Used to verify that specific methods were called with expected parameters
- Active verification of behavior
var emailService = new Mock();
// Act
service.SendWelcomeEmail(userId);
// Assert
emailService.Verify(x => x.Send(It.IsAny(), "Welcome!"), Times.Once);
Faking:
- Working implementation with shortcuts (not production-ready)
- More complex than stubs, simpler than real implementations
- Example: in-memory database instead of real SQL database
public class FakeUserRepository : IUserRepository
{
private List _users = new();
public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
public void Add(User user) => _users.Add(user);
}
Key Difference: Stubs provide data, mocks verify behavior, fakes are simplified implementations.
Related Resources
xUnit.net (Modern, Recommended):
- Most modern and actively maintained
- Used by .NET Core team
- No [SetUp]/[TearDown] attributes (uses constructors/IDisposable)
- Better parallelization support
[Fact]for simple tests,[Theory]for parameterized tests
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
Assert.Equal(expected, calculator.Add(a, b));
}
}
NUnit (Mature, Feature-Rich):
- Oldest and most feature-rich
- Rich assertion library
- Supports parallel test execution
[Test],[TestCase]attributes
[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.That(result, Is.EqualTo(5));
}
[TestCase(2, 3, 5)]
[TestCase(0, 0, 0)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
Assert.That(calculator.Add(a, b), Is.EqualTo(expected));
}
}
MSTest (Microsoft's Framework):
- Built into Visual Studio
- Good integration with Microsoft tools
- Less popular in community
[TestMethod],[DataRow]attributes
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
Personal Preference: xUnit for new projects due to modern design and .NET team support.
Related Resources
Principles for testable code:
1. Dependency Injection:
- Inject dependencies rather than creating them internally
- Enables easy substitution with test doubles
// Bad - Hard to test
public class OrderService
{
public void ProcessOrder(Order order)
{
var repository = new OrderRepository(); // Hard-coded dependency
repository.Save(order);
}
}
// Good - Easy to test
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
}
}
2. Single Responsibility Principle:
- Each class/method should have one reason to change
- Smaller, focused units are easier to test
3. Avoid Static Dependencies:
- Static methods and classes are difficult to mock
- Use interfaces and instance methods
4. Pure Functions When Possible:
- Given the same input, always return the same output
- No side effects
- Easiest to test
5. Separate Logic from Infrastructure:
- Keep business logic separate from database, file system, network calls
- Makes logic testable without external dependencies
6. Avoid Hidden Dependencies:
- Make all dependencies explicit in constructor
- Don't use service locators or global state
7. Keep Methods Small:
- Easier to understand and test
- Single level of abstraction
8. Use Interfaces:
- Program to interfaces, not implementations
- Enables mocking and substitution