Tests - Homelab Automation Dashboard
Architecture de tests complète pour le backend FastAPI et le frontend JavaScript.
Structure
tests/
├── backend/ # Tests Python (pytest)
│ ├── conftest.py # Fixtures partagées (DB, client, mocks)
│ ├── test_routes_auth.py # Tests authentification JWT
│ ├── test_routes_hosts.py # Tests CRUD hôtes
│ ├── test_routes_schedules.py # Tests planificateur
│ ├── test_services_ansible.py # Tests service Ansible
│ ├── test_services_scheduler.py # Tests APScheduler
│ ├── test_services_notification.py # Tests ntfy
│ └── test_websocket.py # Tests WebSocket
├── frontend/ # Tests JavaScript (Vitest)
│ ├── setup.js # Configuration (mocks fetch, WS, localStorage)
│ ├── main.test.js # Tests DashboardManager
│ └── dom.test.js # Tests rendu DOM
└── README.md # Ce fichier
Prérequis
Backend (Python)
pip install pytest pytest-asyncio pytest-cov httpx respx freezegun pytest-mock
Frontend (Node.js)
sudo apt-get install nodejs npm
npm install
Commandes
Exécuter tous les tests
make test-all
# Ou avec coverage:
make test-cov
# Ou séparément:
make test-backend
make test-frontend
Avec couverture
make test-cov
Mode watch (développement)
make test-watch # Frontend Vitest watch
pytest-watch tests/backend # Backend (nécessite pytest-watch)
Tests rapides
make test-quick
Tests backend uniquement
pytest tests/backend -v -m "unit"
# Avec coverage:
pytest tests/backend --cov=app --cov-report=html
Tests frontend uniquement
npm test
# Avec coverage:
npm run test:coverage
Marqueurs pytest
| Marqueur | Description |
|---|---|
@pytest.mark.unit |
Tests unitaires (rapides, isolés) |
@pytest.mark.integration |
Tests d'intégration |
@pytest.mark.slow |
Tests lents (skippés en CI fast) |
Exécuter par marqueur:
pytest tests/backend -m "unit"
pytest tests/backend -m "integration"
pytest tests/backend -m "not slow"
Fixtures Principales (Backend)
db_session
Session SQLAlchemy async avec SQLite in-memory. Rollback automatique après chaque test.
client
AsyncClient httpx avec authentification mockée. Dépendances FastAPI overridées.
unauthenticated_client
AsyncClient sans authentification pour tester les erreurs 401.
mock_ansible_service
Mock complet du service Ansible (pas de subprocess réel).
mock_notification_service
Mock du service ntfy (pas de HTTP réel).
host_factory, user_factory, etc.
Factories pour créer des données de test.
async def test_example(db_session, host_factory):
host = await host_factory.create(db_session, name="test.local")
assert host.name == "test.local"
Mocks Frontend
setupFetchMock(responses)
Configure les réponses fetch mockées:
setupFetchMock({
'/api/hosts': [{ id: 'h1', name: 'server1' }],
'/api/auth/status': { authenticated: true }
});
MockWebSocket
Classe WebSocket mockée avec helpers:
const ws = new MockWebSocket('ws://localhost/ws');
ws._receiveMessage({ type: 'host_updated', data: {...} });
localStorageMock
Mock localStorage avec espions:
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', 'token');
Ajouter un Nouveau Test
Backend
- Créer le fichier
tests/backend/test_<module>.py - Importer les fixtures depuis
conftest.py - Utiliser les marqueurs appropriés
import pytest
from httpx import AsyncClient
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
class TestMyFeature:
async def test_something(self, client: AsyncClient, db_session):
response = await client.get("/api/my-endpoint")
assert response.status_code == 200
Frontend
- Créer le fichier
tests/frontend/<name>.test.js - Importer les utilitaires depuis
setup.js
import { describe, it, expect } from 'vitest';
import { setupFetchMock } from './setup.js';
describe('MyFeature', () => {
it('does something', async () => {
setupFetchMock({ '/api/data': { value: 42 } });
// Test...
});
});
Seuils de Couverture
| Composant | Seuil |
|---|---|
| Backend global | 80% |
| Services critiques | 90% |
| Frontend global | 60% |
CI/CD
Les tests s'exécutent automatiquement via GitHub Actions sur:
- Push sur
mainoudevelop - Pull requests vers
mainoudevelop
Voir .github/workflows/tests.yml
Bonnes Pratiques
- Isolation: Chaque test est indépendant (DB rollback, mocks reset)
- Pas de réseau: Tous les appels HTTP/SSH sont mockés
- Déterminisme: Utiliser
freezegunpour les tests liés au temps - AAA Pattern: Arrange, Act, Assert clairement séparés
- Nommage explicite:
test_<action>_<condition>_<expected_result>