Testing Strategy
Quality software requires comprehensive testing at every level. YeboLearn maintains 70%+ code coverage while ensuring critical user flows work flawlessly in production.
Testing Philosophy
Test Pyramid
/\
/ \
/ E2E \ Few (Critical flows only)
/--------\
/ \
/ Integration \ Some (API contracts, integrations)
/--------------\
/ \
/ Unit Tests \ Many (Business logic, utilities)
/--------------------\Distribution:
- 70% Unit tests (fast, isolated, many)
- 20% Integration tests (moderate speed, dependencies)
- 10% E2E tests (slow, full system, critical flows)
Testing Principles
Fast Feedback
- Unit tests run in <1 second
- Integration tests in <30 seconds
- Full test suite in <5 minutes
- CI runs tests on every commit
Reliable Tests
- No flaky tests (fix or remove)
- Deterministic results
- Isolated from each other
- Clean state before each test
Maintainable Tests
- Clear test names describe behavior
- Test one thing per test
- Readable arrange-act-assert structure
- Minimal test duplication
Valuable Tests
- Test behavior, not implementation
- Cover edge cases and errors
- Validate business requirements
- Catch regressions
Unit Testing
What to Unit Test
Business Logic (Always):
// Quiz scoring algorithm
export function calculateQuizScore(answers: Answer[]): number {
// Complex logic deserves thorough testing
}
// AI prompt construction
export function buildQuizPrompt(topic: string, difficulty: Level): string {
// Critical for AI feature quality
}
// Validation logic
export function validatePaymentAmount(amount: number): ValidationResult {
// Financial calculations must be precise
}Utilities and Helpers (Always):
// Date formatting
export function formatStudentProgress(data: ProgressData): string
// Data transformations
export function parseQuizResults(raw: RawData): QuizResult
// Permission checking
export function canAccessCourse(user: User, course: Course): booleanReact Components (Selectively):
// Complex UI logic
function QuizTimer({ duration, onComplete }) {
// Test timer behavior, countdown, completion
}
// Conditional rendering
function StudentDashboard({ user }) {
// Test different states and permissions
}What NOT to Unit Test
- External library code (trust it works)
- Simple getters/setters
- Constants and configuration
- Trivial pass-through functions
- Database queries (use integration tests)
Unit Test Structure
Arrange-Act-Assert Pattern:
import { calculateQuizScore } from './quiz-scoring';
describe('calculateQuizScore', () => {
it('should return 100% for all correct answers', () => {
// Arrange: Set up test data
const answers = [
{ questionId: '1', correct: true },
{ questionId: '2', correct: true },
{ questionId: '3', correct: true },
];
// Act: Execute the function
const score = calculateQuizScore(answers);
// Assert: Verify the result
expect(score).toBe(100);
});
it('should return 0% for all incorrect answers', () => {
const answers = [
{ questionId: '1', correct: false },
{ questionId: '2', correct: false },
];
const score = calculateQuizScore(answers);
expect(score).toBe(0);
});
it('should calculate partial credit correctly', () => {
const answers = [
{ questionId: '1', correct: true },
{ questionId: '2', correct: false },
{ questionId: '3', correct: true },
{ questionId: '4', correct: false },
];
const score = calculateQuizScore(answers);
expect(score).toBe(50);
});
it('should handle empty answer array', () => {
const answers = [];
const score = calculateQuizScore(answers);
expect(score).toBe(0);
});
});Testing React Components
Component Testing with React Testing Library:
import { render, screen, fireEvent } from '@testing-library/react';
import { QuizQuestion } from './QuizQuestion';
describe('QuizQuestion', () => {
it('should display question text and options', () => {
const question = {
id: '1',
text: 'What is the capital of Lesotho?',
options: ['Maseru', 'Pretoria', 'Mbabane', 'Gaborone'],
};
render(<QuizQuestion question={question} onAnswer={jest.fn()} />);
expect(screen.getByText('What is the capital of Lesotho?')).toBeInTheDocument();
expect(screen.getByText('Maseru')).toBeInTheDocument();
});
it('should call onAnswer when option is selected', () => {
const onAnswer = jest.fn();
const question = {
id: '1',
text: 'What is 2 + 2?',
options: ['3', '4', '5'],
};
render(<QuizQuestion question={question} onAnswer={onAnswer} />);
fireEvent.click(screen.getByText('4'));
expect(onAnswer).toHaveBeenCalledWith('1', '4');
});
it('should disable options after answer is selected', () => {
render(<QuizQuestion question={mockQuestion} onAnswer={jest.fn()} />);
const option = screen.getByText('4');
fireEvent.click(option);
screen.getAllByRole('button').forEach(button => {
expect(button).toBeDisabled();
});
});
});Testing Async Code
Promises and API Calls:
import { fetchStudentProgress } from './api';
describe('fetchStudentProgress', () => {
it('should return student progress data', async () => {
// Mock the API response
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ progress: 75, coursesCompleted: 3 }),
})
) as jest.Mock;
const result = await fetchStudentProgress('student-123');
expect(result.progress).toBe(75);
expect(result.coursesCompleted).toBe(3);
});
it('should handle API errors gracefully', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
) as jest.Mock;
await expect(fetchStudentProgress('student-123')).rejects.toThrow('Network error');
});
});Test Coverage Targets
Minimum Coverage: 70%
- Statements: 70%
- Branches: 70%
- Functions: 70%
- Lines: 70%
Critical Paths: 90%+
- Payment processing
- AI feature logic
- Authentication and authorization
- Quiz scoring and grading
- Data validation
Configuration (jest.config.js):
module.exports = {
coverageThreshold: {
global: {
statements: 70,
branches: 70,
functions: 70,
lines: 70,
},
'./src/features/payments/': {
statements: 90,
branches: 90,
functions: 90,
lines: 90,
},
'./src/features/ai/': {
statements: 85,
branches: 85,
functions: 85,
lines: 85,
},
},
};Integration Testing
What to Integration Test
API Endpoints:
- Request/response validation
- Database interactions
- Error handling
- Authentication/authorization
External Integrations:
- Gemini API for AI features
- Payment gateways (M-Pesa)
- Email service providers
- SMS/WhatsApp messaging
Database Operations:
- Complex queries
- Transactions
- Data integrity
- Migration validation
Integration Test Examples
Testing API Endpoints:
import request from 'supertest';
import { app } from '../app';
import { db } from '../database';
describe('POST /api/quizzes', () => {
beforeEach(async () => {
// Clean database before each test
await db.quiz.deleteMany({});
});
afterAll(async () => {
await db.$disconnect();
});
it('should create a new quiz', async () => {
const quizData = {
title: 'Mathematics Quiz',
subject: 'Math',
difficulty: 'intermediate',
questions: [
{ text: 'What is 2 + 2?', options: ['3', '4', '5'], correctAnswer: '4' },
],
};
const response = await request(app)
.post('/api/quizzes')
.set('Authorization', `Bearer ${validToken}`)
.send(quizData)
.expect(201);
expect(response.body.quiz.title).toBe('Mathematics Quiz');
expect(response.body.quiz.id).toBeDefined();
// Verify database was updated
const dbQuiz = await db.quiz.findUnique({
where: { id: response.body.quiz.id },
});
expect(dbQuiz).toBeDefined();
});
it('should return 401 for unauthenticated requests', async () => {
await request(app)
.post('/api/quizzes')
.send({ title: 'Test Quiz' })
.expect(401);
});
it('should validate required fields', async () => {
const response = await request(app)
.post('/api/quizzes')
.set('Authorization', `Bearer ${validToken}`)
.send({ title: '' }) // Missing required fields
.expect(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({ field: 'questions' })
);
});
});Testing External Services:
import { generateQuiz } from './ai-service';
import { mockGeminiAPI } from '../test-utils/mocks';
describe('AI Quiz Generation Integration', () => {
beforeEach(() => {
mockGeminiAPI.reset();
});
it('should generate quiz from Gemini API', async () => {
mockGeminiAPI.mockResponse({
questions: [
{
question: 'What is photosynthesis?',
options: ['A', 'B', 'C', 'D'],
correctAnswer: 'A',
},
],
});
const quiz = await generateQuiz({
topic: 'Biology',
difficulty: 'easy',
questionCount: 10,
});
expect(quiz.questions).toHaveLength(10);
expect(mockGeminiAPI.calls).toHaveLength(1);
});
it('should handle API rate limits', async () => {
mockGeminiAPI.mockError({ status: 429, message: 'Rate limit exceeded' });
await expect(
generateQuiz({ topic: 'Math', difficulty: 'easy', questionCount: 5 })
).rejects.toThrow('Rate limit exceeded');
});
it('should retry on transient failures', async () => {
mockGeminiAPI
.mockErrorOnce({ status: 500 })
.mockErrorOnce({ status: 500 })
.mockResponse({ questions: [...] });
const quiz = await generateQuiz({
topic: 'History',
difficulty: 'medium',
questionCount: 5,
});
expect(quiz.questions).toHaveLength(5);
expect(mockGeminiAPI.calls).toHaveLength(3); // 2 retries + success
});
});End-to-End Testing
Critical User Flows
Must Test E2E:
- Student registration and login
- Course enrollment and access
- Quiz taking and submission
- Payment processing (M-Pesa)
- AI feature usage (quiz generation, essay grading)
- Progress tracking and certificates
E2E with Playwright:
import { test, expect } from '@playwright/test';
test.describe('Student Quiz Flow', () => {
test('should complete full quiz flow', async ({ page }) => {
// Login
await page.goto('https://dev-api.yebolearn.app/login');
await page.fill('[data-testid="email"]', 'student@test.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
// Navigate to course
await page.click('text=Mathematics 101');
await expect(page).toHaveURL(/.*\/courses\/math-101/);
// Start quiz
await page.click('[data-testid="start-quiz"]');
await expect(page.locator('[data-testid="quiz-question"]')).toBeVisible();
// Answer questions
await page.click('[data-testid="option-b"]');
await page.click('[data-testid="next-question"]');
await page.click('[data-testid="option-a"]');
await page.click('[data-testid="next-question"]');
// Submit quiz
await page.click('[data-testid="submit-quiz"]');
await page.click('[data-testid="confirm-submit"]');
// Verify results
await expect(page.locator('[data-testid="quiz-score"]')).toBeVisible();
const score = await page.textContent('[data-testid="quiz-score"]');
expect(score).toMatch(/\d+%/);
// Verify progress updated
await page.goto('https://dev-api.yebolearn.app/dashboard');
await expect(page.locator('text=Mathematics 101')).toBeVisible();
});
test('should handle quiz timeout', async ({ page }) => {
await loginAsStudent(page);
await page.goto('/courses/math-101/quiz');
// Mock system time to skip ahead
await page.evaluate(() => {
// Fast-forward time
});
await expect(page.locator('text=Time expired')).toBeVisible();
await expect(page.locator('[data-testid="quiz-question"]')).toBeDisabled();
});
});Testing Payment Flow:
test.describe('M-Pesa Payment Integration', () => {
test('should complete course purchase', async ({ page }) => {
await loginAsStudent(page);
await page.goto('/courses/advanced-biology');
// Enroll in paid course
await page.click('[data-testid="enroll-button"]');
await expect(page.locator('text=Select Payment Method')).toBeVisible();
// Select M-Pesa
await page.click('[data-testid="mpesa-option"]');
await page.fill('[data-testid="phone-number"]', '+26878422613');
await page.click('[data-testid="pay-button"]');
// Wait for M-Pesa prompt
await expect(page.locator('text=Check your phone')).toBeVisible();
// Simulate M-Pesa callback (in test environment)
await simulateMpesaCallback({ status: 'success', transactionId: 'TEST123' });
// Verify payment success
await expect(page.locator('text=Payment successful')).toBeVisible({ timeout: 10000 });
// Verify course access granted
await page.goto('/courses/advanced-biology');
await expect(page.locator('[data-testid="course-content"]')).toBeVisible();
});
});Testing Tools
Jest (Unit & Integration)
Configuration:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/test-utils/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/src/test-utils/setup.ts'],
};Running Tests:
# All tests
npm test
# Watch mode (for development)
npm test -- --watch
# Coverage report
npm test -- --coverage
# Specific test file
npm test -- quiz-scoring.test.ts
# Update snapshots
npm test -- -uPlaywright (E2E)
Configuration:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
retries: 2,
use: {
baseURL: 'https://dev-api.yebolearn.app',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'Chrome', use: { browserName: 'chromium' } },
{ name: 'Firefox', use: { browserName: 'firefox' } },
{ name: 'Safari', use: { browserName: 'webkit' } },
],
});Running E2E Tests:
# All E2E tests
npx playwright test
# Headed mode (see browser)
npx playwright test --headed
# Specific test
npx playwright test quiz-flow.spec.ts
# Debug mode
npx playwright test --debug
# Generate test report
npx playwright show-reportQA Process
Manual Testing Checklist
Before Every Release:
- [ ] Critical user flows work end-to-end
- [ ] Payment processing tested in staging
- [ ] AI features generate quality content
- [ ] Mobile responsiveness verified
- [ ] Cross-browser compatibility (Chrome, Firefox, Safari)
- [ ] Performance benchmarks met (page load <2s)
- [ ] No console errors or warnings
- [ ] Accessibility tested (keyboard navigation, screen readers)
Exploratory Testing
Weekly Sessions:
- 2-hour focused testing sessions
- Different team members each week
- Focus areas rotate (payments, AI, UX, mobile)
- Document findings in Linear
Areas to Explore:
- Edge cases not covered by automated tests
- User experience and flow
- Performance under load
- Error handling and recovery
- Mobile and tablet experiences
Test Environments
Local Development
- Use test database with seed data
- Mock external APIs (Gemini, M-Pesa)
- Fast test execution
- Full debugging capability
CI Environment
- Clean state for each run
- Real database (PostgreSQL in Docker)
- Integration tests with real services (dev keys)
- Parallel test execution
Staging Environment
- Production-like configuration
- E2E tests run here
- Real integrations (test mode)
- Manual QA validation
Performance Testing
Load Testing
Tools: k6, Artillery
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 100, // 100 virtual users
duration: '5m',
};
export default function () {
const res = http.get('https://api.yebolearn.app/health');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}Run Load Tests:
# API performance test
k6 run load-test.js
# Stress test (gradual ramp-up)
k6 run --vus 10 --duration 10m stress-test.jsBenchmarks
Target Metrics:
- API response time: <200ms (p95)
- Database queries: <50ms average
- Page load: <2s (including AI features)
- Quiz generation: <5s
- Payment processing: <10s
Test Maintenance
Keeping Tests Healthy
Weekly:
- Review and fix flaky tests
- Update tests for new features
- Remove obsolete tests
- Review coverage reports
Monthly:
- Refactor duplicate test code
- Update test dependencies
- Review E2E test performance
- Optimize slow tests
Handling Flaky Tests
If Test is Flaky:
- Investigate root cause
- Fix if possible (better waits, isolation)
- If unfixable, quarantine or delete
- Never ignore flaky tests
Common Causes:
- Race conditions (add proper waits)
- Shared test state (improve isolation)
- External dependencies (mock or stabilize)
- Timing assumptions (use dynamic waits)
Testing Best Practices
Do:
- Write tests as you code (TDD when appropriate)
- Test behavior, not implementation
- Keep tests fast and isolated
- Use descriptive test names
- Review test coverage regularly
- Run tests before pushing code
Don't:
- Commit failing tests
- Skip tests to make CI pass
- Test framework internals
- Write overly complex tests
- Ignore flaky tests
- Duplicate test logic
Test Users & Demo Data
Seeding Test Data
YeboLearn provides seed scripts to populate test data for all dashboard modes and school types.
Available Seed Scripts:
# Basic seed (Demo High School - Botswana)
npm run seed
# Demo Academy (South Africa - comprehensive demo)
npm run seed:demo
# Comprehensive test users (ALL school types & roles)
npm run seed:testTest User Credentials
Universal Password for all test accounts: Test@123456
ECD Center (Nala's Little Lions)
| Role | Dashboard | |
|---|---|---|
| Admin | admin@nalaecd.test | School Admin |
| Principal | principal@nalaecd.test | School Admin |
| Finance | finance@nalaecd.test | School Admin |
| Caregiver | caregiver1@nalaecd.test | Teacher (ECD mode) |
| Caregiver | caregiver2@nalaecd.test | Teacher (ECD mode) |
| Parent | parent.ecd@nalaecd.test | Parent |
Primary School (Greenwood Primary)
| Role | Dashboard | |
|---|---|---|
| Admin | admin@greenwood.test | School Admin |
| Principal | principal@greenwood.test | School Admin |
| Finance | finance@greenwood.test | School Admin |
| Teacher | teacher@greenwood.test | Teacher (Primary mode) |
| Parent | parent@greenwood.test | Parent |
Secondary School (Manzini High)
| Role | Dashboard | |
|---|---|---|
| Admin | admin@manzini.test | School Admin |
| Principal | principal@manzini.test | School Admin |
| Finance | finance@manzini.test | School Admin |
| Teacher | teacher@manzini.test | Teacher (Secondary mode) |
| Parent | parent@manzini.test | Parent |
| Student | student@manzini.test | Student |
University (Ubuntu University)
| Role | Dashboard | |
|---|---|---|
| Registrar | registrar@ubuntuuni.test | School Admin |
| Dean | dean@ubuntuuni.test | School Admin |
| Bursar | bursar@ubuntuuni.test | School Admin |
| Lecturer | lecturer@ubuntuuni.test | Teacher (Tertiary mode) |
| Parent | parent@ubuntuuni.test | Parent |
| Student | student@ubuntuuni.test | Student |
B2C Home-Only (No School)
| Role | Dashboard | |
|---|---|---|
| Parent | parent.home@yebolearn.test | Parent (Home-Only mode) |
Hybrid Parent (School + Home Children)
| Role | Dashboard | |
|---|---|---|
| Parent | parent.hybrid@yebolearn.test | Parent (Hybrid mode) |
Dashboard Mode Quick Reference
Teacher Dashboard Modes:
- ECD Mode:
caregiver1@nalaecd.test - Primary Mode:
teacher@greenwood.test - Secondary Mode:
teacher@manzini.test - Tertiary Mode:
lecturer@ubuntuuni.test
Parent Dashboard Modes:
- School-Linked:
parent@manzini.test - Home-Only:
parent.home@yebolearn.test - Hybrid:
parent.hybrid@yebolearn.test
Admin Dashboard:
- School Admin:
admin@manzini.test - Principal:
principal@manzini.test - Finance:
finance@manzini.test
Student Dashboard:
- Secondary:
student@manzini.test - University:
student@ubuntuuni.test
Demo Data Credentials
For sales demos and presentations, use the Demo Academy accounts:
Universal Password: Demo@123456
| Role | |
|---|---|
| Admin | demo.admin@demoacademy.edu |
| Teacher | demo.teacher1@demoacademy.edu |
| Parent | demo.parent1@demoacademy.edu |
| Student | demo.student1@demoacademy.edu |
Platform Super Admin
For platform-level administration:
| Role | Password | |
|---|---|---|
| Super Admin | superadmin@yebolearn.com | SuperAdmin@123 |
Resources
Related Documentation
- Code Quality Standards - Coding standards
- CI/CD Pipeline - Automated testing in CI
- Deployment Process - Pre-deployment testing