Unit Tests, Integration Tests & End-to-End Tests
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: κορυφή – πολύ λίγα, ακριβά σε χρόνο, αλλά παρέχουν το υψηλότερο επίπεδο εμπιστοσύνης.
Τηρώντας αυτή τη δομή, εξασφαλίζουμε:
- Γρήγορη feedback loop (unit tests).
- Ανίχνευση σφαλμάτων σε επίπεδο ενσωμάτωσης (integration tests).
- Τελική επαλήθευση λειτουργικότητας (E2E tests).
Τύποι Δοκιμών & Επίπεδα
| Περίπτωση | Τύπος Δοκιμής |
|---|---|
| Business rules | Unit |
| Calculations | Unit |
| Repositories | Integration |
| EF mappings | Integration |
| API wiring | Integration |
| Happy path χρήστη | E2E |
| Smoke tests | E2E |
Κανόνας εμπειρίας
≈ 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