Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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
FluentAssertions - readable assertions

Problem z standardowymi assertions

// ❌ Standard assertions - nieczytelne
[Fact]
public void GetActiveUsers_ReturnsOnlyActiveUsers()
{
    var users = _userService.GetActiveUsers();
    
    // Brzydkie, verbose assertions
    Assert.True(users.Count > 0);
    Assert.True(users.All(u => u.IsActive));
    Assert.False(users.Any(u => u.IsDeleted));
    Assert.Equal("Jan", users[0].Name);
}

FluentAssertions - czytelne jak zdanie

// Install: dotnet add package FluentAssertions

using FluentAssertions;

[Fact]
public void GetActiveUsers_ReturnsOnlyActiveUsers()
{
    // Act
    var users = _userService.GetActiveUsers();
    
    // Assert - readable like English! ✨
    users.Should().NotBeEmpty();
    users.Should().OnlyContain(u => u.IsActive);
    users.Should().NotContain(u => u.IsDeleted);
    users.First().Name.Should().Be("Jan");
}

// Collections
users.Should().HaveCount(5);
users.Should().Contain(u => u.Name == "Jan");
users.Should().BeInAscendingOrder(u => u.Name);

// Strings
user.Name.Should().Be("Jan");
user.Name.Should().StartWith("J");
user.Name.Should().NotBeNullOrWhiteSpace();

// Numbers
order.Total.Should().BeGreaterThan(100);
order.Total.Should().BeInRange(50, 200);

// Dates
order.CreatedAt.Should().BeCloseTo(DateTime.Now, precision: TimeSpan.FromSeconds(1));

// Booleans
user.IsActive.Should().BeTrue();
user.IsDeleted.Should().BeFalse();

// Null
user.Should().NotBeNull();
user.MiddleName.Should().BeNull();

// Types
result.Should().BeOfType<User>();
result.Should().BeAssignableTo<IUser>();

// Exceptions
Action act = () => account.Withdraw(1000);
act.Should().Throw<InsufficientFundsException>()
   .WithMessage("Insufficient funds");

FluentAssertions - complex assertions

// 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!