Développement

Tests automatisés en PHP — De Pest à l'intégration continue

Tests automatisés en PHP — De Pest à l'intégration continue

Les tests automatisés sont souvent négligés en phase de démarrage. Erreur fatale ! Voici comment tester efficacement avec Pest et intégrer ça dans votre CI/CD.

Pourquoi Pest plutôt que PHPUnit ?

Pest est une surcouche moderne de PHPUnit avec une syntaxe bien plus expressive.

// PHPUnit (verbeux)
class ArticleTest extends TestCase
{
    public function test_an_article_can_be_published(): void
    {
        $article = Article::factory()->create(['published' => false]);
        $article->publish();
        $this->assertTrue($article->published);
    }
}

// Pest (élégant)
it('can be published', function () {
    $article = Article::factory()->create(['published' => false]);
    $article->publish();
    expect($article->published)->toBeTrue();
});

Structure des tests

tests/
├── Pest.php           ← Configuration globale
├── Feature/           ← Tests d'intégration (HTTP, DB)
│   ├── ArticleTest.php
│   └── AuthTest.php
└── Unit/              ← Tests unitaires (logique pure)
    ├── ArticleTest.php
    └── MarkdownParserTest.php

Tests Feature avec Laravel

// tests/Feature/ArticleTest.php

use App\Models\Article;
use App\Models\User;

beforeEach(function () {
    $this->admin = User::factory()->create(['is_admin' => true]);
});

describe('Articles publics', function () {
    it('affiche la liste des articles publiés', function () {
        Article::factory()->count(3)->published()->create();
        Article::factory()->count(2)->draft()->create();

        $response = $this->get('/articles');

        $response->assertStatus(200)
                 ->assertSeeText('articles');
    });

    it('retourne 404 pour un article inexistant', function () {
        $this->get('/articles/article-inexistant')
             ->assertStatus(404);
    });
});

describe('Administration', function () {
    it('permet à un admin de créer un article', function () {
        $this->actingAs($this->admin)
             ->post('/admin/articles', [
                 'title'     => 'Mon super article',
                 'content'   => '# Contenu',
                 'published' => true,
             ])
             ->assertRedirect('/admin');

        $this->assertDatabaseHas('articles', [
            'title' => 'Mon super article',
        ]);
    });

    it('refuse la création sans titre', function () {
        $this->actingAs($this->admin)
             ->post('/admin/articles', ['content' => '# Contenu'])
             ->assertSessionHasErrors('title');
    });
});

Tests unitaires

// tests/Unit/ArticleTest.php

use App\Models\Article;

it('estime correctement le temps de lecture', function () {
    // 200 mots = 1 minute
    $content = str_repeat('mot ', 200);
    $article = new Article(['content' => $content]);

    expect($article->estimateReadTime())->toBe(1);
});

it('génère un slug depuis le titre', function () {
    expect(Article::generateSlug('Mon Article Test'))
        ->toBe('mon-article-test');
});

dataset('titres et slugs', [
    ['Hello World', 'hello-world'],
    ['Laravel 11 — Nouveautés', 'laravel-11-nouveautes'],
    ['C++ vs Rust', 'c-vs-rust'],
]);

it('génère des slugs valides', function (string $titre, string $attendu) {
    expect(Article::generateSlug($titre))->toBe($attendu);
})->with('titres et slugs');

GitHub Actions CI/CD

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: pdo_sqlite
          coverage: xdebug

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Copy .env
        run: cp .env.example .env.testing

      - name: Generate key
        run: php artisan key:generate --env=testing

      - name: Run tests with coverage
        run: ./vendor/bin/pest --coverage --min=80

      - name: Upload coverage
        uses: codecov/codecov-action@v3

Bonnes pratiques

  1. Testez le comportement, pas l'implémentation
  2. Utilisez des factories — jamais de données hardcodées
  3. Un test = un comportement — pas de test qui vérifie 10 choses
  4. Utilisez RefreshDatabase — base propre à chaque test
  5. Coverage ≥ 80% — sous 60% c'est insuffisant
// Pest.php
uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');

Commandes utiles

# Lancer tous les tests
./vendor/bin/pest

# Tests en parallèle (beaucoup plus rapide !)
./vendor/bin/pest --parallel

# Avec coverage HTML
./vendor/bin/pest --coverage-html=coverage/

# Filtrer par description
./vendor/bin/pest --filter="admin"

// Commentaires (0)

Connectez-vous pour laisser un commentaire.

Aucun commentaire pour le moment.