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

Published: (February 4, 2026 at 11:25 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

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

Η αυτοματοποιημένη δοκιμή (automated testing) δεν είναι πολυτέλεια, ούτε «nice‑to‑have». Είναι

  • μηχανισμός ανάδρασης,
  • εργαλείο σχεδίασης,
  • ασφαλιστική δικλείδα για τη βιωσιμότητα ενός συστήματος.

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

ΕπίπεδοΠεριγραφή
Unit TestsΔοκιμάζουν τη μικρότερη λογική μονάδα (μέθοδο, κλάση, pure function).
Integration TestsΕλέγχουν τη συνεργασία πολλών components, τη σωστή καλωδίωση (DI, mappings, persistence) και την επικοινωνία με πραγματικές υποδομές.
End‑to‑End (E2E) / System TestsΕπαληθεύουν το σύστημα από άκρη σε άκρη, όπως το βλέπει ο τελικός χρήστης ή client.

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

  • Να επενδύουμε μόνο σε ένα επίπεδο.
  • Να μπερδεύουμε τους ρόλους τους.

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

1. Unit Tests

Τι ελέγχουν

  • Μία μέθοδο, κλάση ή pure function.
  • Χωρίς εξαρτήσεις από:
    • βάσεις δεδομένων,
    • filesystem,
    • network,
    • clocks,
    • random generators.

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

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

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

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

Παράδειγμα domain

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;
    }
}

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);
    }
}

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

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

2. Integration Tests

Τι ελέγχουν

  • Συνεργασία πολλών components.
  • Σωστή καλωδίωση (DI, mappings, persistence).
  • Επικοινωνία με πραγματικές υποδομές (π.χ. SQLite, Testcontainers, EF Core, repositories).

Εδώ δεν mockάρουμε τα πάντα.

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

  • Πιάνουν λάθη που τα 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; }
}

public class AppDbContext : DbContext
{
    public DbSet<OrderEntity> Orders => Set<OrderEntity>();

    public AppDbContext(DbContextOptions options)
        : base(options) { }
}

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);
    }
}

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);
    }
}

3. End‑to‑End (E2E) / System 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);
    }
}

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);
    }
}

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

Η ιδανική κατανομή των δοκιμών ακολουθεί το κλασικό μοντέλο της πυραμίδας:


        │   E2E / System Tests   (5‑10% του συνολικού αριθμού)

        │   Integration Tests    (10‑20%)

        │   Unit Tests           (70‑85%)
        └───────────────────────────────────────
  • Unit Tests: η βάση – πολλά, γρήγορα, αξιόπιστα.
  • Integration Tests: μεσαίο επίπεδο – λιγότερα, αλλά ελέγχουν συνεργασία.
  • E2E Tests: κορυφή – πολύ λίγα, ακριβά σε χρόνο, αλλά παρέχουν το υψηλότερο επίπεδο εμπιστοσύνης.

Τηρώντας αυτή τη δομή, εξασφαλίζουμε:

  1. Γρήγορη feedback loop (unit tests).
  2. Ανίχνευση σφαλμάτων σε επίπεδο ενσωμάτωσης (integration tests).
  3. Τελική επαλήθευση λειτουργικότητας (E2E tests).

Τύποι Δοκιμών & Επίπεδα

ΠερίπτωσηΤύπος Δοκιμής
Business rulesUnit
CalculationsUnit
RepositoriesIntegration
EF mappingsIntegration
API wiringIntegration
Happy path χρήστηE2E
Smoke testsE2E

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

≈ 70 % Unit tests

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

  • Unit tests – για λογική επιχειρηματικών κανόνων και υπολογισμών.
  • Integration tests – για αλληλεπιδράσεις με εξωτερικές εξαρτήσεις (π.χ. αποθήκες, EF mappings, API).
  • E2E tests – για το πλήρες σενάριο χρήστη (happy path) και γρήγορους ελέγχους (smoke).

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

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

Κανένα επίπεδο δεν αντικαθιστά τα άλλα.

✉️ Επικοινωνία: nikosstit@gmail.com

Back to Blog

Related posts

Read more »

10. C# (Static Typing vs Dynamic Typing)

The Real Goal of This Lesson This lesson is not about deciding whether static typing is “better” or “worse”. The real goal is to understand when and why the C...