Skip to main content

Complete Testing & Quality Guide

Overview

Comprehensive testing strategy for the HCMC Traffic Management System covering unit tests, integration tests, E2E tests, performance testing, and code quality standards.

Testing Stack:

  • Python: pytest, pytest-cov, pytest-asyncio, pytest-mock, Locust
  • TypeScript/JavaScript: Jest, React Testing Library, Playwright, Cypress
  • Code Quality: ESLint, Ruff, Black, Prettier, mypy, SonarQube
  • CI/CD: GitHub Actions, Pre-commit hooks

Table of Contents

Unit Testing

  1. Python Unit Tests
  2. TypeScript Unit Tests
  3. Test Coverage

Integration Testing

  1. API Integration Tests
  2. Database Integration
  3. Agent Integration
  4. External Services

E2E Testing

  1. Playwright Setup
  2. User Workflows
  3. Visual Regression

Performance Testing

  1. Load Testing
  2. Stress Testing
  3. Database Performance

Code Quality

  1. Linting
  2. Type Checking
  3. Code Formatting
  4. Security Scanning

CI/CD Integration

  1. Pre-commit Hooks
  2. GitHub Actions
  3. Coverage Reporting

Unit Testing

Python Unit Tests

pytest Configuration

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--cov=src
--cov=agents
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
--asyncio-mode=auto
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
gpu: Tests requiring GPU

Camera Agent Unit Tests

# tests/unit/agents/test_camera_image_fetch.py
import pytest
from unittest.mock import Mock, patch, AsyncMock
from datetime import datetime
import cv2
import numpy as np
from agents.camera_image_fetch import CameraImageFetchAgent

@pytest.fixture
def camera_config():
"""Fixture for camera configuration."""
return {
"id": "CAM_001",
"name": "Camera District 1",
"stream_url": "rtsp://example.com/stream1",
"location": {"lat": 10.7769, "lon": 106.7009}
}

@pytest.fixture
def mock_redis():
"""Mock Redis client."""
redis_mock = Mock()
redis_mock.get = Mock(return_value=None)
redis_mock.setex = Mock(return_value=True)
return redis_mock

@pytest.fixture
def camera_agent(camera_config, mock_redis):
"""Create camera agent instance."""
agent = CameraImageFetchAgent(config=camera_config)
agent.redis_client = mock_redis
return agent

class TestCameraImageFetchAgent:
"""Test suite for Camera Image Fetch Agent."""

def test_initialization(self, camera_agent, camera_config):
"""Test agent initialization."""
assert camera_agent.camera_id == "CAM_001"
assert camera_agent.stream_url == camera_config["stream_url"]
assert camera_agent.location == camera_config["location"]

@pytest.mark.asyncio
async def test_fetch_image_success(self, camera_agent):
"""Test successful image fetching."""
# Create dummy image
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)

with patch('cv2.VideoCapture') as mock_cap:
mock_cap.return_value.read.return_value = (True, dummy_image)
mock_cap.return_value.release = Mock()

result = await camera_agent.fetch_image()

assert result is not None
assert result["camera_id"] == "CAM_001"
assert result["timestamp"] is not None
assert isinstance(result["image"], np.ndarray)

@pytest.mark.asyncio
async def test_fetch_image_failure(self, camera_agent):
"""Test image fetch failure handling."""
with patch('cv2.VideoCapture') as mock_cap:
mock_cap.return_value.read.return_value = (False, None)

result = await camera_agent.fetch_image()

assert result is None

def test_cache_hit(self, camera_agent, mock_redis):
"""Test cache retrieval."""
cached_data = b'cached_image_data'
mock_redis.get.return_value = cached_data

result = camera_agent.get_cached_image()

assert result == cached_data
mock_redis.get.assert_called_once()

def test_cache_miss(self, camera_agent, mock_redis):
"""Test cache miss scenario."""
mock_redis.get.return_value = None

result = camera_agent.get_cached_image()

assert result is None

@pytest.mark.parametrize("quality,expected_size", [
(50, 5000),
(75, 10000),
(100, 20000)
])
def test_image_compression(self, camera_agent, quality, expected_size):
"""Test image compression with different quality levels."""
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)

compressed = camera_agent.compress_image(dummy_image, quality)

assert len(compressed) > 0
assert len(compressed) < expected_size

Accident Detection Unit Tests

# tests/unit/agents/test_accident_detection.py
import pytest
from unittest.mock import Mock, patch
import numpy as np
from agents.accident_detection import AccidentDetectionAgent

@pytest.fixture
def yolo_model_mock():
"""Mock YOLOX model."""
model = Mock()
model.predict = Mock(return_value=[Mock(boxes=Mock(xyxy=np.array([[100, 100, 200, 200]])))])
return model

@pytest.fixture
def detection_agent(yolo_model_mock):
"""Create accident detection agent with YOLOX."""
# Using YOLOX model - mock the exp.get_model() call
with patch('yolox.exp.get_exp') as mock_get_exp:
mock_get_exp.return_value.get_model.return_value = yolo_model_mock
agent = AccidentDetectionAgent()
return agent

class TestAccidentDetectionAgent:
"""Test suite for Accident Detection Agent."""

def test_initialization(self, detection_agent):
"""Test agent initialization."""
assert detection_agent.model is not None
assert detection_agent.confidence_threshold == 0.5

def test_detect_accident_positive(self, detection_agent):
"""Test accident detection with positive result."""
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)

result = detection_agent.detect(dummy_image)

assert result is not None
assert "accident_detected" in result
assert result["accident_detected"] is True
assert "confidence" in result

def test_detect_accident_negative(self, detection_agent, yolo_model_mock):
"""Test no accident detected."""
yolo_model_mock.predict.return_value = [Mock(boxes=Mock(xyxy=np.array([])))]
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)

result = detection_agent.detect(dummy_image)

assert result["accident_detected"] is False

@pytest.mark.parametrize("confidence,should_detect", [
(0.3, False),
(0.5, True),
(0.8, True),
(0.95, True)
])
def test_confidence_threshold(self, detection_agent, confidence, should_detect):
"""Test confidence threshold filtering."""
detection_agent.confidence_threshold = 0.5

result = detection_agent._filter_by_confidence(confidence)

assert result == should_detect

TypeScript Unit Tests

Jest Configuration

// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/**/*.stories.tsx'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
}
};

Component Unit Tests

// src/components/__tests__/TrafficMap.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TrafficMap } from '../TrafficMap';
import '@testing-library/jest-dom';

// Mock MapLibre GL / react-map-gl
jest.mock('../map', () => ({
MapContainer: ({ children }: any) => <div data-testid="map-container">{children}</div>,
Marker: ({ children }: any) => <div data-testid="marker">{children}</div>,
Popup: ({ children }: any) => <div data-testid="popup">{children}</div>,
Polyline: () => <div data-testid="polyline" />,
useMap: () => ({ setView: jest.fn(), getZoom: () => 13 }),
}));

describe('TrafficMap Component', () => {
const mockCameras = [
{
id: 'CAM_001',
name: 'Camera 1',
location: { lat: 10.7769, lon: 106.7009 },
status: 'active'
},
{
id: 'CAM_002',
name: 'Camera 2',
location: { lat: 10.7800, lon: 106.7100 },
status: 'inactive'
}
];

const mockAccidents = [
{
id: 'ACC_001',
location: { lat: 10.7750, lon: 106.7050 },
severity: 'severe',
timestamp: new Date().toISOString()
}
];

it('renders map container', () => {
render(<TrafficMap cameras={mockCameras} accidents={[]} />);

expect(screen.getByTestId('map-container')).toBeInTheDocument();
expect(screen.getByTestId('tile-layer')).toBeInTheDocument();
});

it('displays camera markers', () => {
render(<TrafficMap cameras={mockCameras} accidents={[]} />);

const markers = screen.getAllByTestId('marker');
expect(markers).toHaveLength(2);
});

it('displays accident markers', () => {
render(<TrafficMap cameras={[]} accidents={mockAccidents} />);

const markers = screen.getAllByTestId('marker');
expect(markers).toHaveLength(1);
});

it('filters cameras by status', async () => {
const { rerender } = render(
<TrafficMap cameras={mockCameras} accidents={[]} filterStatus="active" />
);

let markers = screen.getAllByTestId('marker');
expect(markers).toHaveLength(1);

rerender(<TrafficMap cameras={mockCameras} accidents={[]} filterStatus="all" />);

markers = screen.getAllByTestId('marker');
expect(markers).toHaveLength(2);
});

it('calls onCameraClick when camera is clicked', async () => {
const handleCameraClick = jest.fn();
const user = userEvent.setup();

render(
<TrafficMap
cameras={mockCameras}
accidents={[]}
onCameraClick={handleCameraClick}
/>
);

const marker = screen.getAllByTestId('marker')[0];
await user.click(marker);

expect(handleCameraClick).toHaveBeenCalledWith('CAM_001');
});
});

API Hook Tests

// src/hooks/__tests__/useCameras.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { useCameras } from '../useCameras';

const server = setupServer(
rest.get('/api/cameras', (req, res, ctx) => {
return res(
ctx.json({
data: [
{ id: 'CAM_001', name: 'Camera 1', status: 'active' },
{ id: 'CAM_002', name: 'Camera 2', status: 'active' }
]
})
);
})
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('useCameras Hook', () => {
it('fetches cameras successfully', async () => {
const { result } = renderHook(() => useCameras());

expect(result.current.loading).toBe(true);

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(result.current.cameras).toHaveLength(2);
expect(result.current.error).toBeNull();
});

it('handles fetch error', async () => {
server.use(
rest.get('/api/cameras', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);

const { result } = renderHook(() => useCameras());

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(result.current.cameras).toHaveLength(0);
expect(result.current.error).not.toBeNull();
});
});

Integration Testing

API Integration Tests

FastAPI Integration Tests

# tests/integration/test_api_integration.py
import pytest
from httpx import AsyncClient
from src.main import app

@pytest.mark.integration
@pytest.mark.asyncio
async def test_camera_list_endpoint():
"""Test camera list API endpoint."""
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/cameras")

assert response.status_code == 200
data = response.json()
assert "data" in data
assert isinstance(data["data"], list)

@pytest.mark.integration
@pytest.mark.asyncio
async def test_camera_details_endpoint():
"""Test camera details endpoint."""
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/cameras/CAM_001")

assert response.status_code == 200
data = response.json()
assert data["id"] == "CAM_001"
assert "name" in data
assert "location" in data

@pytest.mark.integration
@pytest.mark.asyncio
async def test_accident_report_creation():
"""Test creating accident report."""
accident_data = {
"location": {"lat": 10.7769, "lon": 106.7009},
"severity": "moderate",
"description": "Two vehicle collision",
"camera_id": "CAM_001"
}

async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/api/accidents", json=accident_data)

assert response.status_code == 201
data = response.json()
assert data["severity"] == "moderate"
assert "id" in data

@pytest.mark.integration
@pytest.mark.asyncio
async def test_authentication_flow():
"""Test authentication workflow."""
async with AsyncClient(app=app, base_url="http://test") as client:
# Login
login_response = await client.post(
"/api/auth/login",
json={"username": "testuser", "password": "testpass"}
)
assert login_response.status_code == 200
tokens = login_response.json()
assert "access_token" in tokens

# Access protected endpoint
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
protected_response = await client.get("/api/user/profile", headers=headers)
assert protected_response.status_code == 200

Database Integration

MongoDB Integration Tests

# tests/integration/test_mongodb_integration.py
import pytest
from motor.motor_asyncio import AsyncIOMotorClient
from datetime import datetime

@pytest.fixture
async def mongo_client():
"""Create MongoDB test client."""
client = AsyncIOMotorClient("mongodb://localhost:27017")
db = client.test_traffic_db
yield db
await client.drop_database("test_traffic_db")
client.close()

@pytest.mark.integration
@pytest.mark.asyncio
async def test_insert_camera_data(mongo_client):
"""Test inserting camera data."""
camera_data = {
"id": "CAM_TEST_001",
"name": "Test Camera",
"location": {"lat": 10.7769, "lon": 106.7009},
"status": "active",
"created_at": datetime.utcnow()
}

result = await mongo_client.cameras.insert_one(camera_data)
assert result.inserted_id is not None

# Verify insertion
found = await mongo_client.cameras.find_one({"id": "CAM_TEST_001"})
assert found["name"] == "Test Camera"

@pytest.mark.integration
@pytest.mark.asyncio
async def test_query_accidents_by_severity(mongo_client):
"""Test querying accidents by severity."""
accidents = [
{"id": "ACC_001", "severity": "minor", "location": {"lat": 10.77, "lon": 106.70}},
{"id": "ACC_002", "severity": "severe", "location": {"lat": 10.78, "lon": 106.71}},
{"id": "ACC_003", "severity": "moderate", "location": {"lat": 10.79, "lon": 106.72}}
]

await mongo_client.accidents.insert_many(accidents)

# Query severe accidents
severe_accidents = await mongo_client.accidents.find({"severity": "severe"}).to_list(length=100)
assert len(severe_accidents) == 1
assert severe_accidents[0]["id"] == "ACC_002"

E2E Testing

Playwright Setup

Playwright Configuration

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'test-results/junit.xml' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

User Workflow Tests

// e2e/citizen-report.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Citizen Report Workflow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test('user submits accident report', async ({ page }) => {
// Navigate to report page
await page.click('button:has-text("Report Accident")');
await expect(page).toHaveURL('/report');

// Fill form
await page.fill('input[name="location"]', '10.7769, 106.7009');
await page.selectOption('select[name="severity"]', 'moderate');
await page.fill('textarea[name="description"]', 'Two car collision at intersection');

// Upload image
await page.setInputFiles('input[type="file"]', 'test-data/accident-image.jpg');

// Submit
await page.click('button:has-text("Submit Report")');

// Verify success
await expect(page.locator('.success-message')).toBeVisible();
await expect(page.locator('.success-message')).toContainText('Report submitted successfully');
});

test('displays real-time accident alerts', async ({ page }) => {
await page.goto('/map');

// Wait for map to load
await page.waitForSelector('.maplibregl-map');

// Trigger accident (simulate WebSocket message)
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('accident-alert', {
detail: {
id: 'ACC_TEST_001',
severity: 'severe',
location: { lat: 10.7769, lon: 106.7009 }
}
}));
});

// Verify alert appears
await expect(page.locator('.accident-alert')).toBeVisible();
await expect(page.locator('.accident-alert')).toContainText('severe');
});

test('filters cameras by district', async ({ page }) => {
await page.goto('/cameras');

// Wait for cameras to load
await page.waitForSelector('.camera-card');

const initialCount = await page.locator('.camera-card').count();

// Apply district filter
await page.selectOption('select[name="district"]', 'District 1');

// Verify filtered results
const filteredCount = await page.locator('.camera-card').count();
expect(filteredCount).toBeLessThan(initialCount);

// Verify all visible cameras are in District 1
const districts = await page.locator('.camera-card .district').allTextContents();
districts.forEach(district => {
expect(district).toContain('District 1');
});
});
});

Performance Testing

Load Testing

Locust Load Test

# tests/performance/locustfile.py
from locust import HttpUser, task, between

class TrafficSystemUser(HttpUser):
"""Simulate user behavior for load testing."""

wait_time = between(1, 3)

def on_start(self):
"""Login before tests."""
response = self.client.post("/api/auth/login", json={
"username": "loadtest_user",
"password": "loadtest_pass"
})
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}

@task(3)
def view_cameras(self):
"""View camera list (most common operation)."""
self.client.get("/api/cameras", headers=self.headers)

@task(2)
def view_accidents(self):
"""View accident list."""
self.client.get("/api/accidents", headers=self.headers)

@task(1)
def view_traffic_flow(self):
"""View traffic flow data."""
self.client.get("/api/traffic/flow", headers=self.headers)

@task(1)
def view_camera_details(self):
"""View specific camera details."""
self.client.get("/api/cameras/CAM_001", headers=self.headers)

@task(1)
def submit_citizen_report(self):
"""Submit citizen report."""
report_data = {
"location": {"lat": 10.7769, "lon": 106.7009},
"severity": "moderate",
"description": "Test report from load testing"
}
self.client.post("/api/citizen-reports", json=report_data, headers=self.headers)

Running Load Tests

# Run load test with 100 users
locust -f tests/performance/locustfile.py --host=http://localhost:8000 --users 100 --spawn-rate 10

# Run headless with specific duration
locust -f tests/performance/locustfile.py --host=http://localhost:8000 \
--users 1000 --spawn-rate 50 --run-time 10m --headless

Code Quality

Linting

ESLint Configuration

// .eslintrc.js
module.exports = {
extends: [
'react-app',
'react-app/jest',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'no-console': ['warn', { allow: ['warn', 'error'] }]
}
};

Ruff Configuration (Replaces Pylint, Flake8, isort)

# ruff.toml
[tool.ruff]
line-length = 120
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "W", "C90", "I", "N", "B", "S"]
ignore = ["E501", "S101"]

Note (2025-12): Migrated from pylint/flake8/isort to Ruff for 10-100x faster linting.


Pre-commit Hooks

Pre-commit Configuration

# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=1000']

- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.9

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0
hooks:
- id: ruff
args: ['--fix']
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [types-all]

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, json, css, markdown]

License

MIT License - Copyright (c) 2025 UIP Contributors (Nguyễn Nhật Quang, Nguyễn Việt Hoàng, Nguyễn Đình Anh Tuấn)

See LICENSE for details.