DEV Community

nikosst
nikosst

Posted on

Unit Tests, Integration Tests & End-to-End Tests

Το πότε, πώς, πού και κυρίως γιατί της αυτοματοποιημένης δοκιμής λογισμικού

Η αυτοματοποιημένη δοκιμή (automated testing) δεν είναι πολυτέλεια, ούτε «nice to have». Είναι μηχανισμός ανάδρασης, εργαλείο σχεδίασης και ασφαλιστική δικλείδα για τη βιωσιμότητα ενός συστήματος.

Στην πράξη, τα tests χωρίζονται συνήθως σε τρία βασικά επίπεδα:

  • Unit Tests
  • Integration Tests
  • End-to-End (E2E) / System Tests

Δεν είναι ανταγωνιστικά· είναι συμπληρωματικά. Το λάθος που γίνεται συχνά είναι είτε:

να επενδύουμε μόνο σε ένα επίπεδο

είτε να μπερδεύουμε τους ρόλους τους

Ας τα δούμε ένα-ένα, με παραδείγματα σε C# (.NET).


1. Unit Tests
Τι ελέγχουν

Τα Unit Tests ελέγχουν τη μικρότερη λογική μονάδα του κώδικα:

  • μια μέθοδο
  • μια κλάση
  • έναν pure function

Χωρίς εξαρτήσεις από:

  • βάσεις δεδομένων
  • filesystem
  • network
  • clocks
  • random generators

Αν κάτι δεν είναι deterministic, mockάρεται.


Γιατί είναι σημαντικά

  • Είναι γρήγορα (milliseconds)
  • Τρέχουν συνεχώς (IDE, CI)
  • Τεκμηριώνουν συμπεριφορά
  • Επιβάλλουν καλό design (SRP, low coupling)

Ένα σύστημα δύσκολο να γίνει unit tested είναι σχεδόν πάντα κακοσχεδιασμένο.


Παράδειγμα domain

Ας πούμε ότι έχουμε ένα απλό domain rule:

public class Order
{
    public decimal TotalAmount { get; }

    public Order(decimal totalAmount)
    {
        TotalAmount = totalAmount;
    }

    public decimal CalculateDiscount()
    {
        if (TotalAmount >= 1000)
            return TotalAmount * 0.10m;

        if (TotalAmount >= 500)
            return TotalAmount * 0.05m;

        return 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Unit Test (xUnit)

public class OrderTests
{
    [Fact]
    public void Orders_over_1000_get_10_percent_discount()
    {
        var order = new Order(1200m);

        var discount = order.CalculateDiscount();

        Assert.Equal(120m, discount);
    }

    [Fact]
    public void Orders_between_500_and_999_get_5_percent_discount()
    {
        var order = new Order(600m);

        var discount = order.CalculateDiscount();

        Assert.Equal(30m, discount);
    }

    [Fact]
    public void Orders_below_500_get_no_discount()
    {
        var order = new Order(300m);

        var discount = order.CalculateDiscount();

        Assert.Equal(0m, discount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Χαρακτηριστικά:

  • Καμία εξάρτηση
  • Καθαρό Arrange-Act-Assert
  • Deterministic αποτέλεσμα

2. Integration Tests
Τι ελέγχουν
Τα Integration Tests ελέγχουν:

  • τη συνεργασία πολλών components
  • τη σωστή καλωδίωση (DI, mappings, persistence)
  • την επικοινωνία με πραγματικές υποδομές

Εδώ ΔΕΝ mockάρουμε τα πάντα.
Αντιθέτως, θέλουμε:

  • πραγματική βάση (π.χ. SQLite / Testcontainers)
  • πραγματικό EF Core
  • πραγματικά repositories

Γιατί είναι σημαντικά

Πιάνουν λάθη που unit tests δεν μπορούν

Εντοπίζουν:

  • λάθος mappings
  • migrations
  • serialization issues
  • misconfigured services

“Works on my machine” bugs σκοτώνονται εδώ.


Παράδειγμα: Repository + EF Core

public class OrderEntity
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class AppDbContext : DbContext
{
    public DbSet<OrderEntity> Orders => Set<OrderEntity>();

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }
}
Enter fullscreen mode Exit fullscreen mode
public class OrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(OrderEntity order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }

    public async Task<OrderEntity?> GetByIdAsync(int id)
    {
        return await _context.Orders.FindAsync(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration Test με In-Memory SQLite

public class OrderRepositoryTests
{
    [Fact]
    public async Task Can_save_and_retrieve_order()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite("Filename=:memory:")
            .Options;

        using var context = new AppDbContext(options);
        context.Database.OpenConnection();
        context.Database.EnsureCreated();

        var repository = new OrderRepository(context);

        var order = new OrderEntity { TotalAmount = 500m };

        await repository.AddAsync(order);

        var savedOrder = await repository.GetByIdAsync(order.Id);

        Assert.NotNull(savedOrder);
        Assert.Equal(500m, savedOrder!.TotalAmount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Σημαντικό:
Αυτό ΔΕΝ είναι unit test — και δεν πρέπει να είναι.


3. End-to-End (E2E) / System Tests
Τι ελέγχουν

Τα E2E tests ελέγχουν:

  • το σύστημα από άκρη σε άκρη
  • όπως το βλέπει ο τελικός χρήστης ή client

Παράδειγμα:

  • HTTP request → controller
  • business logic
  • database
  • response

Χωρίς mocks. Όλα αληθινά.


Γιατί είναι σημαντικά (και επικίνδυνα)

✅ Επιβεβαιώνουν ότι:

  • το σύστημα δουλεύει πραγματικά
  • τα integrations είναι σωστά

❌ Αλλά:

  • είναι αργά
  • είναι εύθραυστα
  • δύσκολα στο debugging

Ένα failing E2E test σου λέει ότι κάτι χάλασε, όχι τι.


Παράδειγμα: ASP.NET Core API

Controller:

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly AppDbContext _context;

    public OrdersController(AppDbContext context)
    {
        _context = context;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        var order = new OrderEntity
        {
            TotalAmount = request.TotalAmount
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        return Ok(order.Id);
    }
}
Enter fullscreen mode Exit fullscreen mode

E2E Test με WebApplicationFactory

public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Can_create_order_via_api()
    {
        var payload = new
        {
            TotalAmount = 750
        };

        var response = await _client.PostAsJsonAsync("/api/orders", payload);

        response.EnsureSuccessStatusCode();

        var orderId = await response.Content.ReadFromJsonAsync<int>();

        Assert.True(orderId > 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Πυραμίδα Tests (Testing Pyramid)

Ιδανική κατανομή:

    ▲
    │   E2E (λίγα)
    │
    │   Integration (αρκετά)
    │
    │   Unit (πολλά)
    ▼
Enter fullscreen mode Exit fullscreen mode

Κανόνας εμπειρίας:

~70% Unit
~20% Integration
~10% E2E


Πότε χρησιμοποιούμε τι

Περίπτωση Test
Business rules Unit
Calculations Unit
Repositories Integration
EF mappings Integration
API wiring Integration
Happy path χρήστη E2E
Smoke tests E2E

Συμπέρασμα (senior take)

  • Unit tests προστατεύουν το design
  • Integration tests προστατεύουν την υποδομή
  • E2E tests προστατεύουν την εμπιστοσύνη

Κανένα επίπεδο δεν αντικαθιστά τα άλλα.
Όλα μαζί σχηματίζουν ένα δίχτυ ασφαλείας, όχι μια ψευδαίσθηση ποιότητας.


nikosstit@gmail.com

Top comments (0)