Skip to content

Tests et qualité

400+ tests 85% couverture

Notre stratégie de tests repose sur la pyramide des tests :

/\
/ \ Tests E2E (10%)
/----\ Scénarios utilisateur complets
/ \
/--------\ Tests d'intégration (20%)
/ \ Communication entre composants
/------------\
/ \ Tests unitaires (70%)
/----------------\ Fonctions et classes isolées
TypeNombreCouverture
Tests unitaires250+85%
Tests de feature150+90%
Tests d’intégration50+-
Total400+-
phpunit.xml
<phpunit>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">app</directory>
</include>
<exclude>
<directory>app/Console</directory>
</exclude>
</coverage>
</phpunit>
class CardServiceTest extends TestCase
{
private CardService $service;
private MockObject $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = $this->createMock(CardRepositoryInterface::class);
$this->service = new CardService($this->repository);
}
public function test_it_validates_card_type(): void
{
$this->expectException(InvalidCardTypeException::class);
$this->service->create([
'type' => 'invalid_type',
'question' => 'Test',
'answer' => 'Answer',
]);
}
public function test_it_calculates_difficulty_correctly(): void
{
$card = new Card([
'question' => 'Simple question',
'answer' => 'Short',
]);
$difficulty = $this->service->calculateDifficulty($card);
$this->assertEquals(1, $difficulty);
}
}
class CardFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'collection_id' => Collection::factory(),
'type' => $this->faker->randomElement([
'flashcard', 'mcq', 'truefalse'
]),
'question' => $this->faker->sentence() . '?',
'answer' => $this->faker->paragraph(),
'options' => null,
'metadata' => [],
];
}
public function mcq(): static
{
return $this->state(fn () => [
'type' => 'mcq',
'options' => [
['text' => 'Option A', 'correct' => true],
['text' => 'Option B', 'correct' => false],
['text' => 'Option C', 'correct' => false],
['text' => 'Option D', 'correct' => false],
],
]);
}
}
OutilUsage
JestTest runner
React Native Testing LibraryTests de composants
MSWMock des requêtes API
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { Flashcard } from '@/components/cards/Flashcard';
describe('Flashcard', () => {
it('should display question initially', () => {
const { getByText } = render(
<Flashcard
question="What is React?"
answer="A JavaScript library"
/>
);
expect(getByText('What is React?')).toBeTruthy();
});
it('should flip and show answer on press', async () => {
const { getByText, getByTestId } = render(
<Flashcard
question="What is React?"
answer="A JavaScript library"
/>
);
fireEvent.press(getByTestId('flashcard'));
await waitFor(() => {
expect(getByText('A JavaScript library')).toBeTruthy();
});
});
it('should be accessible', () => {
const { getByA11yLabel } = render(
<Flashcard
question="What is React?"
answer="A JavaScript library"
/>
);
expect(getByA11yLabel('Retourner la carte')).toBeTruthy();
});
});
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useCollectionsStore } from '@/stores/collectionsStore';
describe('useCollectionsStore', () => {
beforeEach(() => {
useCollectionsStore.getState().reset();
});
it('should fetch collections', async () => {
const { result } = renderHook(() => useCollectionsStore());
await act(async () => {
await result.current.fetchCollections();
});
expect(result.current.collections).toHaveLength(3);
expect(result.current.isLoading).toBe(false);
});
it('should handle fetch error', async () => {
server.use(
rest.get('/api/v1/collections', (req, res, ctx) => {
return res(ctx.status(500));
})
);
const { result } = renderHook(() => useCollectionsStore());
await expect(
act(() => result.current.fetchCollections())
).rejects.toThrow();
});
});
import pytest
from unittest.mock import Mock, patch
from services.card_generation import CardGenerationService
class TestCardGenerationService:
@pytest.fixture
def service(self):
return CardGenerationService()
@pytest.fixture
def mock_llm(self):
with patch('services.card_generation.get_llm') as mock:
mock.return_value = Mock()
yield mock.return_value
def test_extract_concepts(self, service, mock_llm):
mock_llm.generate.return_value = {
"concepts": ["PHP", "Web development", "Server-side"]
}
content = "PHP is a server-side scripting language..."
concepts = service.extract_concepts(content)
assert len(concepts) == 3
assert "PHP" in concepts
def test_generate_question_from_concept(self, service, mock_llm):
mock_llm.generate.return_value = {
"question": "What is PHP?",
"answer": "A programming language",
"difficulty": 1
}
question = service.generate_question(
concept="PHP",
context="Programming languages"
)
assert question["question"] == "What is PHP?"
assert question["difficulty"] == 1
def test_validate_question_quality(self, service):
good_question = {
"question": "What is the main purpose of PHP?",
"answer": "Server-side web scripting",
"difficulty": 2
}
assert service.validate_quality(good_question) > 0.7
def test_reject_low_quality_question(self, service):
bad_question = {
"question": "???",
"answer": "ok",
"difficulty": 1
}
assert service.validate_quality(bad_question) < 0.5
.github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
backend-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug
- name: Install dependencies
run: composer install --no-progress
- name: Run tests
run: php artisan test --coverage
env:
DB_CONNECTION: pgsql
DB_HOST: localhost
DB_DATABASE: testing
- name: Upload coverage
uses: codecov/codecov-action@v3
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
  1. Tous les tests doivent passer ✅
  2. Couverture de code ne doit pas diminuer 📊
  3. Code review approuvée par au moins 1 personne 👀
  4. Pas de conflits avec la branche cible 🔀
  • ❌ Couverture insuffisante (~40%)
  • ❌ Pas de tests d’intégration
  • ❌ Tests manuels uniquement pour le frontend
  • ❌ Pas de CI/CD automatisé
  • ✅ Couverture à 85%+
  • ✅ Tests d’intégration complets
  • ✅ Tests automatisés du frontend
  • ✅ CI/CD sur chaque PR

La qualité n’est pas négociable.