Skip to content

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):

typescript
// 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):

typescript
// 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): boolean

React Components (Selectively):

typescript
// 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:

typescript
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:

typescript
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:

typescript
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):

javascript
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:

typescript
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:

typescript
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:

  1. Student registration and login
  2. Course enrollment and access
  3. Quiz taking and submission
  4. Payment processing (M-Pesa)
  5. AI feature usage (quiz generation, essay grading)
  6. Progress tracking and certificates

E2E with Playwright:

typescript
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"]', '[email protected]');
    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:

typescript
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:

javascript
// 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:

bash
# 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 -- -u

Playwright (E2E)

Configuration:

typescript
// 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:

bash
# 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-report

QA 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

javascript
// 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:

bash
# API performance test
k6 run load-test.js

# Stress test (gradual ramp-up)
k6 run --vus 10 --duration 10m stress-test.js

Benchmarks

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:

  1. Investigate root cause
  2. Fix if possible (better waits, isolation)
  3. If unfixable, quarantine or delete
  4. 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:

bash
# 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:test

Test User Credentials

Universal Password for all test accounts: Test@123456

ECD Center (Nala's Little Lions)

RoleEmailDashboard
Admin[email protected]School Admin
Principal[email protected]School Admin
Finance[email protected]School Admin
Caregiver[email protected]Teacher (ECD mode)
Caregiver[email protected]Teacher (ECD mode)
Parent[email protected]Parent

Primary School (Greenwood Primary)

RoleEmailDashboard
Admin[email protected]School Admin
Principal[email protected]School Admin
Finance[email protected]School Admin
Teacher[email protected]Teacher (Primary mode)
Parent[email protected]Parent

Secondary School (Manzini High)

RoleEmailDashboard
Admin[email protected]School Admin
Principal[email protected]School Admin
Finance[email protected]School Admin
Teacher[email protected]Teacher (Secondary mode)
Parent[email protected]Parent
Student[email protected]Student

University (Ubuntu University)

RoleEmailDashboard
Registrar[email protected]School Admin
Dean[email protected]School Admin
Bursar[email protected]School Admin
Lecturer[email protected]Teacher (Tertiary mode)
Parent[email protected]Parent
Student[email protected]Student

B2C Home-Only (No School)

RoleEmailDashboard
Parent[email protected]Parent (Home-Only mode)

Hybrid Parent (School + Home Children)

RoleEmailDashboard
Parent[email protected]Parent (Hybrid mode)

Dashboard Mode Quick Reference

Teacher Dashboard Modes:

Parent Dashboard Modes:

Admin Dashboard:

Student Dashboard:

Demo Data Credentials

For sales demos and presentations, use the Demo Academy accounts:

Universal Password: Demo@123456

RoleEmail
Admin[email protected]
Teacher[email protected]
Parent[email protected]
Student[email protected]

Platform Super Admin

For platform-level administration:

RoleEmailPassword
Super Admin[email protected]SuperAdmin@123

Resources

YeboLearn - Empowering African Education