TDD a Laravel: escriure tests que realment importen

Més enllà del testing ritualista cap a validació que importa
25.11.2025 — Liquid Team — 8 min read

La paradoxa de la cobertura

L'ecosistema Laravel promou el testing mitjançant PHPUnit i Pest, però molts projectes mostren una paradoxa: alta cobertura de tests amb baixa confiança en la correcció del sistema. Això sorgeix de testejar detalls d'implementació en lloc de comportament, centrant-se en getters i setters trivials, i descuidant punts crítics d'integració.

Els tests efectius han de validar tres dominis: invariants de negoci (regles que s'han de mantenir independentment de la implementació), límits d'integració (punts on interactuen sistemes externs), i casos extrems (escenaris on la lògica es ramifica o falla).

Lògica de negoci: més enllà del CRUD

La majoria d'aplicacions Laravel testegen operacions CRUD exhaustivament mentre ignoren regles de domini. Considera un sistema d'inventari e-commerce on la invariant crítica és la consistència de l'stock.

En lloc de testejar que un producte té un atribut d'stock, testeja la regla de negoci:

test('no es pot fer comanda quan stock insuficient', function () {
    $product = Product::factory()->create(['stock' => 2]);
    
    expect(fn() => Order::create([
        'product_id' => $product->id,
        'quantity' => 5
    ]))->toThrow(InsufficientStockException::class);
    
    expect($product->fresh()->stock)->toBe(2); // Sense deducció parcial
});

test('deducció d\'stock és atòmica amb comandes concurrents', function () {
    $product = Product::factory()->create(['stock' => 10]);
    
    DB::transaction(function () use ($product) {
        Order::create(['product_id' => $product->id, 'quantity' => 6]);
        Order::create(['product_id' => $product->id, 'quantity' => 6]);
    });
    
    // Un hauria de fallar, l'stock mai ha de ser negatiu
    expect($product->fresh()->stock)->toBeGreaterThanOrEqual(0);
});

Aquests tests asseguren que les regles de negoci es mantenen sota condicions normals i concurrents, validant correcció del sistema en lloc d'artefactes d'implementació.

Límits d'integració: testejar dependències externes

Serveis externs com passarel·les de pagament, motors de cerca i APIs representen punts de fallada. En lloc de mockejar indiscriminadament, testeja el compliment de contractes en aquests límits:

test('servei de cerca retorna productes amb SKU exacte', function () {
    $product = Product::factory()->create([
        'sku' => 'WINE-RIOJA-2019',
        'name' => 'Rioja Reserve'
    ]);
    
    $product->searchable();
    
    $results = Product::search('WINE-RIOJA-2019')->get();
    
    expect($results)->toHaveCount(1)
        ->and($results->first()->id)->toBe($product->id);
});

test('fallada de passarel·la de pagament reverteix creació de comanda', function () {
    Queue::fake();
    Http::fake([
        'payment-gateway.com/*' => Http::response(['error' => 'Card declined'], 402)
    ]);
    
    expect(fn() => Order::createWithPayment([
        'amount' => 100,
        'card_token' => 'tok_invalid'
    ]))->toThrow(PaymentFailedException::class);
    
    expect(Order::count())->toBe(0);
    Queue::assertNothingPushed();
});

Aquests tests validen comportament en límits del sistema on les fallades són més probables i costoses, assegurant integritat transaccional a través de dependències externes.

Feature tests: fluxos d'usuari sobre detalls HTTP

Els feature tests han de validar fluxos d'usuari complets, no només codis d'estat HTTP. En lloc de testejar que un endpoint retorna 200, testeja tot el flux de negoci:

test('usuari convidat completa compra i rep confirmació', function () {
    $product = Product::factory()->create(['price' => 5000]);
    
    Mail::fake();
    
    $this->post('/checkout', [
        'product_id' => $product->id,
        'email' => 'customer@example.com',
        'payment_method' => 'card'
    ])->assertRedirect('/order/confirmation');
    
    $order = Order::where('email', 'customer@example.com')->first();
    
    expect($order)
        ->not->toBeNull()
        ->and($order->total)->toBe(5000)
        ->and($order->status)->toBe('paid');
    
    Mail::assertSent(OrderConfirmation::class, fn($mail) => 
        $mail->hasTo('customer@example.com')
    );
});

Aquest test valida tot el workflow: persistència, processament de pagament, enviament d'email i transicions d'estat—assegurant que el sistema funciona com esperen els usuaris.

Integritat de dades: testejar restriccions

Les restriccions de base de dades sovint queden sense testejar fins que ocorren fallades en producció. Testeja invariants crítiques a nivell de base de dades:

test('SKUs duplicats es rebutgen a nivell de base de dades', function () {
    Product::factory()->create(['sku' => 'DUPLICATE-SKU']);
    
    expect(fn() => Product::factory()->create(['sku' => 'DUPLICATE-SKU']))
        ->toThrow(QueryException::class);
});

test('total de comanda coincideix amb línies després d\'actualitzacions', function () {
    $order = Order::factory()->create();
    $order->items()->createMany([
        ['price' => 1000, 'quantity' => 2],
        ['price' => 1500, 'quantity' => 1],
    ]);
    
    $order->recalculateTotal();
    expect($order->total)->toBe(3500);
    
    $order->items()->first()->update(['quantity' => 3]);
    $order->recalculateTotal();
    
    expect($order->fresh()->total)->toBe(4500);
});

Aquests tests asseguren que les regles d'integritat s'apliquen consistentment, capturant errors abans que arribin a producció.

Casos extrems: condicions límit

Les fallades crítiques ocorren en límits. Testeja'ls explícitament per capturar escenaris que causen incidents en producció:

test('paginació gestiona resultats buits', function () {
    $this->get('/products?page=999')
        ->assertOk()
        ->assertJson(['data' => []]);
});

test('operacions massives gestionen fallades parcials correctament', function () {
    $validProduct = Product::factory()->create(['stock' => 10]);
    $invalidProduct = Product::factory()->create(['stock' => 0]);
    
    $results = ProductService::bulkOrder([
        ['product_id' => $validProduct->id, 'quantity' => 5],
        ['product_id' => $invalidProduct->id, 'quantity' => 1],
    ]);
    
    expect($results['successful'])->toHaveCount(1)
        ->and($results['failed'])->toHaveCount(1)
        ->and($validProduct->fresh()->stock)->toBe(5);
});

Testejar casos extrems com resultats buits, fallades parcials i límits de zona horària prevé comportaments inesperats en producció.

Què no testejar

Evita testejar funcionalitat del framework, internals de paquets third-party, accessors trivials i mètodes privats. En lloc de testejar que un producte té un accessor de nom, testeja com s'utilitza aquest nom en lògica de negoci:

// No testegis això
test('producte té accessor de nom', function () {
    $product = new Product(['name' => 'Wine']);
    expect($product->name)->toBe('Wine');
});

// Testeja això en el seu lloc
test('cerca de producte indexa nom per queries full-text', function () {
    $product = Product::factory()->create(['name' => 'Rioja Reserve Wine']);
    $product->searchable();
    
    expect(Product::search('Rioja')->get())->toContain($product);
});

El segon test valida comportament real del sistema del qual depenen els usuaris, mentre el primer simplement confirma funcionalitat bàsica de Laravel.

L'objectiu no és el percentatge de cobertura sinó la cobertura de camins crítics—els escenaris on la fallada imposa el major cost en usuaris i lògica de negoci.
Liquid Team
Equip de Desenvolupament

Conclusió

TDD efectiu a Laravel requereix distingir entre tests que verifiquen artefactes d'implementació i aquells que validen comportament del sistema. En centrar-se en invariants de negoci, límits d'integració i casos extrems, els desenvolupadors construeixen suites de tests que proporcionen confiança genuïna mentre romanen resilients al refactoring.

Aquest enfocament transforma el testing d'un ritual d'assolir mètriques de cobertura a una pràctica estratègica que protegeix funcionalitat crítica i experiència d'usuari. Els tests que importen són aquells que capturen fallades reals abans que arribin a producció.

💡 Quant costa desenvolupar una app per al teu negoci? Descobreix els preus