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
- Testez le comportement, pas l'implémentation
- Utilisez des factories — jamais de données hardcodées
- Un test = un comportement — pas de test qui vérifie 10 choses
- Utilisez
RefreshDatabase— base propre à chaque test - 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"
Aucun commentaire pour le moment.