Testing to podstawa profesjonalnego developmentu! W 2015 roku używałeś MSTest lub NUnit. W 2026 roku standard to xUnit z FluentAssertions, Moq/NSubstitute do mockowania, i Test-Driven Development jako metodologia!
📅 Timeline - Testing w .NET
2005 - MSTest w Visual Studio
2007 - NUnit popularny
2008 - Moq library dla mocking
2013 - 🔥 xUnit.net - modern testing framework
2015+ - FluentAssertions dla readable assertions
2020+ - xUnit jako standard w .NET Core/5+
🔍 Dlaczego testy?
Confidence - wiesz że kod działa Refactoring - możesz zmieniać kod bez strachu Documentation - testy pokazują jak używać kodu Bug prevention - łapiesz błędy wcześniej Design - testy wymuszają dobry design (testable code = good code)
Test Frameworks - xUnit vs NUnit vs MSTest
Feature
xUnit
NUnit
MSTest
Popularność 2026
⭐⭐⭐⭐⭐ Default
⭐⭐⭐⭐ Popularny
⭐⭐⭐ Legacy
Test attribute
[Fact]
[Test]
[TestMethod]
Parametrized
[Theory]
[TestCase]
[DataRow]
Setup/Teardown
Constructor/Dispose
[SetUp]
[TestInitialize]
Isolation
Nowa instancja per test
Shared instance
Shared instance
.NET Core/5+
✅ Preferred
✅ Supported
✅ Supported
xUnit - recommended w 2026
// Install: dotnet add package xunit
// dotnet add package xunit.runner.visualstudio
using Xunit;
public class CalculatorTests
{
// [Fact] - pojedynczy test
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
// [Theory] - parametrized test (wiele przypadków)
[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
[InlineData(100, 200, 300)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
int result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
// Uruchom: dotnet test
Setup/Teardown - xUnit style
// xUnit - constructor dla setup, Dispose dla teardown
public class DatabaseTests : IDisposable
{
private readonly DbContext _context;
// Constructor - wywoływany PRZED KAŻDYM testem
public DatabaseTests()
{
_context = new DbContext("test-connection");
_context.Database.EnsureCreated();
}
[Fact]
public void SaveUser_ValidUser_SavesToDatabase()
{
var user = new User { Name = "Jan" };
_context.Users.Add(user);
_context.SaveChanges();
Assert.Equal(1, _context.Users.Count());
}
// Dispose - wywoływane PO KAŻDYM teście
public void Dispose()
{
_context.Database.EnsureDeleted();
_context.Dispose();
}
}
// Każdy test ma ŚWIEŻĄ instancję - full isolation! ✨
Arrange-Act-Assert Pattern (AAA)
AAA - standard structure
// Arrange-Act-Assert - standard test structure
[Fact]
public void ProcessOrder_ValidOrder_SendsConfirmationEmail()
{
// Arrange - przygotuj test data i dependencies
var emailService = new Mock<IEmailService>();
var orderService = new OrderService(emailService.Object);
var order = new Order
{
Id = 1,
CustomerEmail = "jan@example.com",
Total = 100.50m
};
// Act - wykonaj testowaną akcję
orderService.ProcessOrder(order);
// Assert - sprawdź czy efekt jest prawidłowy
emailService.Verify(
e => e.SendConfirmation("jan@example.com", It.IsAny<string>()),
Times.Once
);
}
// AAA = czytelne, konsekwentne testy! ✨
Dobre praktyki AAA
// ✅ GOOD - clear AAA structure
[Fact]
public void Withdraw_SufficientFunds_DecreasesBalance()
{
// Arrange
var account = new BankAccount(initialBalance: 1000);
// Act
account.Withdraw(200);
// Assert
Assert.Equal(800, account.Balance);
}
// ❌ BAD - mixed arrange/act/assert
[Fact]
public void BadTest()
{
var account = new BankAccount(1000); // Arrange
account.Withdraw(200); // Act
var balance = account.Balance; // ???
account.Withdraw(100); // Act again?
Assert.Equal(700, account.Balance); // Assert
// Niejasna struktura! Co testujemy?
}
// ✅ GOOD - test name describes behavior
[Fact]
public void Withdraw_InsufficientFunds_ThrowsInsufficientFundsException()
{
// Arrange
var account = new BankAccount(initialBalance: 100);
// Act & Assert (dla exceptions)
Assert.Throws<InsufficientFundsException>(() => account.Withdraw(200));
}
// Test name format: MethodName_Scenario_ExpectedBehavior
Mocking - Moq i NSubstitute
Po co mocking?
// Problem - dependencies blokują testowanie
public class OrderService
{
private readonly IEmailService _emailService; // External dependency
private readonly IPaymentGateway _paymentGateway; // External dependency
private readonly IRepository _repository; // Database dependency
public OrderService(IEmailService email, IPaymentGateway payment, IRepository repo)
{
_emailService = email;
_paymentGateway = payment;
_repository = repo;
}
public void ProcessOrder(Order order)
{
_paymentGateway.Charge(order.Total); // Nie chcesz PRAWDZIWEJ płatności w teście!
_repository.Save(order); // Nie chcesz PRAWDZIWEJ bazy danych!
_emailService.SendConfirmation(order); // Nie chcesz PRAWDZIWEGO emaila!
}
}
// Rozwiązanie: MOCK dependencies! ✨
Moq - najpopularniejsza library
// Install: dotnet add package Moq
using Moq;
[Fact]
public void ProcessOrder_ValidOrder_ChargesPayment()
{
// Arrange - create mocks
var mockPayment = new Mock<IPaymentGateway>();
var mockRepo = new Mock<IRepository>();
var mockEmail = new Mock<IEmailService>();
var orderService = new OrderService(
mockEmail.Object,
mockPayment.Object,
mockRepo.Object
);
var order = new Order { Total = 100.50m };
// Act
orderService.ProcessOrder(order);
// Assert - verify mock was called
mockPayment.Verify(
p => p.Charge(100.50m),
Times.Once
);
}
// Mock = fake implementation dla testów! ✨
Moq - Setup i Verify
// Setup - określ co mock zwraca
[Fact]
public void GetUser_ExistingUser_ReturnsUser()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
// Setup - gdy wywołasz GetById(1), zwróć tego usera
mockRepo.Setup(r => r.GetById(1))
.Returns(new User { Id = 1, Name = "Jan" });
var userService = new UserService(mockRepo.Object);
// Act
var user = userService.GetUser(1);
// Assert
Assert.Equal("Jan", user.Name);
}
// Verify - sprawdź czy mock został wywołany
[Fact]
public void DeleteUser_ExistingUser_DeletesFromRepository()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
var userService = new UserService(mockRepo.Object);
// Act
userService.DeleteUser(1);
// Assert - verify metoda została wywołana
mockRepo.Verify(r => r.Delete(1), Times.Once);
}
// It.IsAny - match any argument
[Fact]
public void SaveUser_AnyUser_CallsSaveOnRepository()
{
var mockRepo = new Mock<IUserRepository>();
var userService = new UserService(mockRepo.Object);
userService.SaveUser(new User { Name = "Jan" });
// Verify z It.IsAny - nie obchodzi nas konkretny user
mockRepo.Verify(r => r.Save(It.IsAny<User>()), Times.Once);
}
// It.Is - match z warunkiem
mockRepo.Verify(
r => r.Save(It.Is<User>(u => u.Name == "Jan")),
Times.Once
);
Moq - async methods
// Mocking async methods
[Fact]
public async Task GetUserAsync_ExistingUser_ReturnsUser()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
// Setup async method - ReturnsAsync
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new User { Id = 1, Name = "Jan" });
var userService = new UserService(mockRepo.Object);
// Act
var user = await userService.GetUserAsync(1);
// Assert
Assert.Equal("Jan", user.Name);
}
// Throwing exceptions async
mockRepo.Setup(r => r.GetByIdAsync(999))
.ThrowsAsync(new NotFoundException());
NSubstitute - alternatywa dla Moq
// Install: dotnet add package NSubstitute
using NSubstitute;
[Fact]
public void ProcessOrder_ValidOrder_ChargesPayment()
{
// Arrange - create substitute (mock)
var paymentGateway = Substitute.For<IPaymentGateway>();
var repository = Substitute.For<IRepository>();
var emailService = Substitute.For<IEmailService>();
var orderService = new OrderService(emailService, paymentGateway, repository);
var order = new Order { Total = 100.50m };
// Act
orderService.ProcessOrder(order);
// Assert - Received
paymentGateway.Received(1).Charge(100.50m);
}
// Setup return value
var userRepo = Substitute.For<IUserRepository>();
userRepo.GetById(1).Returns(new User { Id = 1, Name = "Jan" });
// Async
userRepo.GetByIdAsync(1).Returns(Task.FromResult(new User { Id = 1 }));
// NSubstitute vs Moq - obie dobre, wybór to preference
// Moq: bardziej verbose, więcej kontroli
// NSubstitute: bardziej fluent, czytelniejsze
// Object comparison
var expected = new User { Id = 1, Name = "Jan", Email = "jan@example.com" };
var actual = _userService.GetUser(1);
// BeEquivalentTo - porównuje properties (nie references)
actual.Should().BeEquivalentTo(expected);
// Exclude properties z porównania
actual.Should().BeEquivalentTo(expected, options =>
options.Excluding(u => u.CreatedAt));
// Collection assertions
var users = _userService.GetAll();
users.Should()
.HaveCount(3)
.And.OnlyContain(u => u.IsActive)
.And.Contain(u => u.Name == "Jan");
// Custom assertion message
users.Should().HaveCount(3, because: "we added 3 users in setup");
// Error: Expected users to have 3 item(s) because we added 3 users in setup, but found 2.
Test-Driven Development (TDD)
TDD - Red-Green-Refactor cycle
🔍 TDD = Test-Driven Development
Red: Napisz test (który failuje - kod nie istnieje) Green: Napisz najprostszy kod żeby test przeszedł Refactor: Popraw kod (testy nadal przechodzą) Repeat: Następna funkcjonalność!
// TDD Example - Calculator
// STEP 1: RED - napisz test (failuje)
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator(); // ❌ Calculator nie istnieje!
int result = calculator.Add(2, 3);
result.Should().Be(5);
}
// Uruchom: dotnet test → RED (compilation error)
// STEP 2: GREEN - najprostszy kod żeby test przeszedł
public class Calculator
{
public int Add(int a, int b)
{
return 5; // Hardcoded - ale test przechodzi! ✅
}
}
// Uruchom: dotnet test → GREEN ✅
// STEP 3: REFACTOR - dodaj więcej testów, popraw implementację
[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();
calculator.Add(a, b).Should().Be(expected);
}
// Teraz hardcoded 5 nie działa → refactor implementation
public class Calculator
{
public int Add(int a, int b)
{
return a + b; // Proper implementation! ✅
}
}
// STEP 4: REPEAT - następna funkcjonalność (Subtract)
TDD - kompleksowy przykład
// TDD: Bank Account
// RED - test dla Deposit
[Fact]
public void Deposit_PositiveAmount_IncreasesBalance()
{
var account = new BankAccount(initialBalance: 100);
account.Deposit(50);
account.Balance.Should().Be(150);
}
// GREEN - minimal implementation
public class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
}
// RED - test dla validation
[Fact]
public void Deposit_NegativeAmount_ThrowsException()
{
var account = new BankAccount(100);
Action act = () => account.Deposit(-50);
act.Should().Throw<ArgumentException>()
.WithMessage("Amount must be positive");
}
// GREEN - add validation
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
Balance += amount;
}
// RED - test dla Withdraw
[Fact]
public void Withdraw_SufficientFunds_DecreasesBalance()
{
var account = new BankAccount(100);
account.Withdraw(30);
account.Balance.Should().Be(70);
}
// GREEN - implement Withdraw
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
if (amount > Balance)
throw new InsufficientFundsException();
Balance -= amount;
}
// TDD = testy PROWADZĄ design kodu! ✨
Zalety TDD
// ✅ Zalety TDD:
// 1. Wymusza testable design
// - Musisz myśleć o interfejsach
// - Musisz myśleć o dependencies
// - Code jest ŁATWIEJSZY do testowania z natury
// 2. Dokumentacja
// - Testy pokazują JAK używać kodu
// - Testy pokazują JAKIE są use cases
// 3. Confidence
// - Wiesz że kod działa (bo test przeszedł)
// - Możesz refaktorować bez strachu
// 4. Less debugging
// - Łapiesz błędy wcześniej
// - Testy są SZYBSZE niż debugowanie
// 5. Better code quality
// - Kod jest bardziej modular
// - Single Responsibility Principle
// - Dependency Injection naturally
// ❌ Kiedy NIE używać TDD:
// - Prototyping (szybkie eksperymenty)
// - UI code (trudne do testowania)
// - Legacy code (najpierw refactor, potem TDD)
// - Bardzo proste CRUD bez logiki
Podsumowanie
✅ xUnit - modern, recommended framework w 2026
✅ NUnit, MSTest - alternatywny, nadal popularny
✅ Arrange-Act-Assert - standard test structure
✅ Moq/NSubstitute - mocking dependencies
✅ FluentAssertions - readable assertions jak zdania
✅ TDD - Red-Green-Refactor, testy prowadzą design
✅ Best practices - test naming, isolation, one assert per test
Następny wpis: Performance - wydajność i optymalizacja!