Skip to content

Testing Strategies

Comprehensive testing guide for mini programs, covering unit testing, integration testing, end-to-end testing, performance testing, and quality assurance practices.

Testing Fundamentals

Testing Pyramid

  • Unit Tests: Fast, isolated tests for individual components
  • Integration Tests: Tests for component interactions and API integrations
  • End-to-End Tests: Full user journey testing
  • Manual Testing: Exploratory and usability testing

Testing Principles

  • Test Early and Often: Continuous testing throughout development
  • Test Automation: Automated testing for regression prevention
  • Test Coverage: Comprehensive coverage of critical functionality
  • Test Maintainability: Easy-to-maintain and understand tests

Unit Testing

Component Testing

javascript
// Unit testing for mini program components
import { render, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, jest } from '@jest/globals';
import UserProfile from '../components/UserProfile';

describe('UserProfile Component', () => {
  const mockUser = {
    id: '123',
    name: 'John Doe',
    email: 'john@example.com',
    avatar: 'https://example.com/avatar.jpg'
  };

  it('should render user information correctly', () => {
    const { getByText, getByAltText } = render(
      <UserProfile user={mockUser} />
    );

    expect(getByText('John Doe')).toBeInTheDocument();
    expect(getByText('john@example.com')).toBeInTheDocument();
    expect(getByAltText('User avatar')).toHaveAttribute('src', mockUser.avatar);
  });

  it('should handle edit button click', async () => {
    const mockOnEdit = jest.fn();
    const { getByRole } = render(
      <UserProfile user={mockUser} onEdit={mockOnEdit} />
    );

    const editButton = getByRole('button', { name: /edit/i });
    fireEvent.click(editButton);

    await waitFor(() => {
      expect(mockOnEdit).toHaveBeenCalledWith(mockUser.id);
    });
  });

  it('should display loading state', () => {
    const { getByTestId } = render(
      <UserProfile user={null} loading={true} />
    );

    expect(getByTestId('loading-spinner')).toBeInTheDocument();
  });

  it('should handle error state', () => {
    const errorMessage = 'Failed to load user';
    const { getByText } = render(
      <UserProfile user={null} error={errorMessage} />
    );

    expect(getByText(errorMessage)).toBeInTheDocument();
  });
});

Service Testing

javascript
// Testing service layer and business logic
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import UserService from '../services/UserService';
import ApiClient from '../utils/ApiClient';

// Mock API client
jest.mock('../utils/ApiClient');
const mockApiClient = ApiClient as jest.Mocked<typeof ApiClient>;

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    userService = new UserService();
    jest.clearAllMocks();
  });

  describe('getUserProfile', () => {
    it('should return user profile successfully', async () => {
      const mockUserData = {
        id: '123',
        name: 'John Doe',
        email: 'john@example.com'
      };

      mockApiClient.get.mockResolvedValue({
        data: mockUserData,
        status: 200
      });

      const result = await userService.getUserProfile('123');

      expect(mockApiClient.get).toHaveBeenCalledWith('/users/123');
      expect(result).toEqual(mockUserData);
    });

    it('should handle API errors gracefully', async () => {
      const errorMessage = 'User not found';
      mockApiClient.get.mockRejectedValue(new Error(errorMessage));

      await expect(userService.getUserProfile('invalid-id'))
        .rejects.toThrow(errorMessage);
    });

    it('should cache user data', async () => {
      const mockUserData = { id: '123', name: 'John Doe' };
      mockApiClient.get.mockResolvedValue({ data: mockUserData });

      // First call
      await userService.getUserProfile('123');
      // Second call
      await userService.getUserProfile('123');

      // API should only be called once due to caching
      expect(mockApiClient.get).toHaveBeenCalledTimes(1);
    });
  });

  describe('updateUserProfile', () => {
    it('should update user profile successfully', async () => {
      const userId = '123';
      const updateData = { name: 'Jane Doe' };
      const updatedUser = { id: userId, ...updateData };

      mockApiClient.put.mockResolvedValue({
        data: updatedUser,
        status: 200
      });

      const result = await userService.updateUserProfile(userId, updateData);

      expect(mockApiClient.put).toHaveBeenCalledWith(
        `/users/${userId}`,
        updateData
      );
      expect(result).toEqual(updatedUser);
    });

    it('should validate input data', async () => {
      const invalidData = { email: 'invalid-email' };

      await expect(userService.updateUserProfile('123', invalidData))
        .rejects.toThrow('Invalid email format');
    });
  });
});

Utility Function Testing

javascript
// Testing utility functions and helpers
import { describe, it, expect } from '@jest/globals';
import {
  formatDate,
  validateEmail,
  calculateAge,
  debounce
} from '../utils/helpers';

describe('Utility Functions', () => {
  describe('formatDate', () => {
    it('should format date correctly', () => {
      const date = new Date('2023-12-25T10:30:00Z');
      expect(formatDate(date, 'YYYY-MM-DD')).toBe('2023-12-25');
      expect(formatDate(date, 'MM/DD/YYYY')).toBe('12/25/2023');
    });

    it('should handle invalid dates', () => {
      expect(formatDate(null)).toBe('');
      expect(formatDate(undefined)).toBe('');
      expect(formatDate('invalid')).toBe('Invalid Date');
    });
  });

  describe('validateEmail', () => {
    it('should validate correct email addresses', () => {
      expect(validateEmail('test@example.com')).toBe(true);
      expect(validateEmail('user.name+tag@domain.co.uk')).toBe(true);
    });

    it('should reject invalid email addresses', () => {
      expect(validateEmail('invalid-email')).toBe(false);
      expect(validateEmail('@domain.com')).toBe(false);
      expect(validateEmail('test@')).toBe(false);
      expect(validateEmail('')).toBe(false);
    });
  });

  describe('calculateAge', () => {
    it('should calculate age correctly', () => {
      const birthDate = new Date('1990-01-01');
      const referenceDate = new Date('2023-01-01');
      expect(calculateAge(birthDate, referenceDate)).toBe(33);
    });

    it('should handle edge cases', () => {
      const birthDate = new Date('2000-12-31');
      const referenceDate = new Date('2001-01-01');
      expect(calculateAge(birthDate, referenceDate)).toBe(0);
    });
  });

  describe('debounce', () => {
    it('should debounce function calls', (done) => {
      let callCount = 0;
      const debouncedFn = debounce(() => {
        callCount++;
      }, 100);

      // Call multiple times quickly
      debouncedFn();
      debouncedFn();
      debouncedFn();

      // Should not have been called yet
      expect(callCount).toBe(0);

      // Wait for debounce delay
      setTimeout(() => {
        expect(callCount).toBe(1);
        done();
      }, 150);
    });
  });
});

Integration Testing

API Integration Testing

javascript
// Testing API integrations and data flow
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
import app from '../app';
import { setupTestDatabase, cleanupTestDatabase } from '../test-utils/database';

describe('User API Integration', () => {
  beforeAll(async () => {
    await setupTestDatabase();
  });

  afterAll(async () => {
    await cleanupTestDatabase();
  });

  describe('POST /api/users', () => {
    it('should create a new user', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'securePassword123'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(String),
        name: userData.name,
        email: userData.email
      });
      expect(response.body.password).toBeUndefined();
    });

    it('should validate required fields', async () => {
      const invalidData = {
        name: 'John Doe'
        // Missing email and password
      };

      const response = await request(app)
        .post('/api/users')
        .send(invalidData)
        .expect(400);

      expect(response.body.errors).toContain('Email is required');
      expect(response.body.errors).toContain('Password is required');
    });

    it('should prevent duplicate email addresses', async () => {
      const userData = {
        name: 'Jane Doe',
        email: 'duplicate@example.com',
        password: 'password123'
      };

      // Create first user
      await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      // Try to create duplicate
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409);

      expect(response.body.error).toBe('Email already exists');
    });
  });

  describe('GET /api/users/:id', () => {
    it('should retrieve user by ID', async () => {
      // Create a user first
      const createResponse = await request(app)
        .post('/api/users')
        .send({
          name: 'Test User',
          email: 'test@example.com',
          password: 'password123'
        });

      const userId = createResponse.body.id;

      // Retrieve the user
      const response = await request(app)
        .get(`/api/users/${userId}`)
        .expect(200);

      expect(response.body).toMatchObject({
        id: userId,
        name: 'Test User',
        email: 'test@example.com'
      });
    });

    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/non-existent-id')
        .expect(404);

      expect(response.body.error).toBe('User not found');
    });
  });
});

Database Integration Testing

javascript
// Testing database operations and data persistence
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { UserRepository } from '../repositories/UserRepository';
import { DatabaseConnection } from '../database/connection';
import { createTestUser, cleanupTestData } from '../test-utils/fixtures';

describe('UserRepository Integration', () => {
  let userRepository: UserRepository;
  let dbConnection: DatabaseConnection;

  beforeEach(async () => {
    dbConnection = new DatabaseConnection(process.env.TEST_DATABASE_URL);
    await dbConnection.connect();
    userRepository = new UserRepository(dbConnection);
  });

  afterEach(async () => {
    await cleanupTestData(dbConnection);
    await dbConnection.disconnect();
  });

  describe('createUser', () => {
    it('should create user in database', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        hashedPassword: 'hashed_password'
      };

      const createdUser = await userRepository.createUser(userData);

      expect(createdUser).toMatchObject({
        id: expect.any(String),
        name: userData.name,
        email: userData.email,
        createdAt: expect.any(Date),
        updatedAt: expect.any(Date)
      });

      // Verify user exists in database
      const foundUser = await userRepository.findById(createdUser.id);
      expect(foundUser).toMatchObject(userData);
    });

    it('should enforce unique email constraint', async () => {
      const userData = {
        name: 'John Doe',
        email: 'duplicate@example.com',
        hashedPassword: 'hashed_password'
      };

      await userRepository.createUser(userData);

      await expect(userRepository.createUser(userData))
        .rejects.toThrow('Email already exists');
    });
  });

  describe('updateUser', () => {
    it('should update user data', async () => {
      const user = await createTestUser(userRepository);
      const updateData = { name: 'Updated Name' };

      const updatedUser = await userRepository.updateUser(user.id, updateData);

      expect(updatedUser.name).toBe('Updated Name');
      expect(updatedUser.updatedAt).not.toEqual(user.updatedAt);
    });

    it('should handle partial updates', async () => {
      const user = await createTestUser(userRepository);
      const originalEmail = user.email;

      await userRepository.updateUser(user.id, { name: 'New Name' });
      const updatedUser = await userRepository.findById(user.id);

      expect(updatedUser.name).toBe('New Name');
      expect(updatedUser.email).toBe(originalEmail);
    });
  });

  describe('findByEmail', () => {
    it('should find user by email', async () => {
      const user = await createTestUser(userRepository);

      const foundUser = await userRepository.findByEmail(user.email);

      expect(foundUser).toMatchObject({
        id: user.id,
        email: user.email
      });
    });

    it('should return null for non-existent email', async () => {
      const foundUser = await userRepository.findByEmail('nonexistent@example.com');
      expect(foundUser).toBeNull();
    });
  });
});

End-to-End Testing

User Journey Testing

javascript
// End-to-end testing with Playwright
import { test, expect } from '@playwright/test';

test.describe('User Registration and Login Flow', () => {
  test('should complete full user registration and login', async ({ page }) => {
    // Navigate to registration page
    await page.goto('/register');

    // Fill registration form
    await page.fill('[data-testid="name-input"]', 'John Doe');
    await page.fill('[data-testid="email-input"]', 'john@example.com');
    await page.fill('[data-testid="password-input"]', 'securePassword123');
    await page.fill('[data-testid="confirm-password-input"]', 'securePassword123');

    // Submit registration
    await page.click('[data-testid="register-button"]');

    // Verify registration success
    await expect(page.locator('[data-testid="success-message"]'))
      .toContainText('Registration successful');

    // Navigate to login page
    await page.goto('/login');

    // Fill login form
    await page.fill('[data-testid="email-input"]', 'john@example.com');
    await page.fill('[data-testid="password-input"]', 'securePassword123');

    // Submit login
    await page.click('[data-testid="login-button"]');

    // Verify successful login
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="user-name"]'))
      .toContainText('John Doe');
  });

  test('should handle registration validation errors', async ({ page }) => {
    await page.goto('/register');

    // Submit empty form
    await page.click('[data-testid="register-button"]');

    // Verify validation errors
    await expect(page.locator('[data-testid="name-error"]'))
      .toContainText('Name is required');
    await expect(page.locator('[data-testid="email-error"]'))
      .toContainText('Email is required');
    await expect(page.locator('[data-testid="password-error"]'))
      .toContainText('Password is required');
  });

  test('should handle login with invalid credentials', async ({ page }) => {
    await page.goto('/login');

    await page.fill('[data-testid="email-input"]', 'invalid@example.com');
    await page.fill('[data-testid="password-input"]', 'wrongpassword');
    await page.click('[data-testid="login-button"]');

    await expect(page.locator('[data-testid="error-message"]'))
      .toContainText('Invalid email or password');
  });
});

test.describe('E-commerce Shopping Flow', () => {
  test('should complete product purchase', async ({ page }) => {
    // Login first
    await page.goto('/login');
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');

    // Browse products
    await page.goto('/products');
    await page.click('[data-testid="product-card"]:first-child');

    // Add to cart
    await page.click('[data-testid="add-to-cart-button"]');
    await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');

    // Go to cart
    await page.click('[data-testid="cart-icon"]');
    await expect(page.locator('[data-testid="cart-item"]')).toBeVisible();

    // Proceed to checkout
    await page.click('[data-testid="checkout-button"]');

    // Fill shipping information
    await page.fill('[data-testid="address-input"]', '123 Main St');
    await page.fill('[data-testid="city-input"]', 'New York');
    await page.fill('[data-testid="zip-input"]', '10001');

    // Fill payment information
    await page.fill('[data-testid="card-number-input"]', '4111111111111111');
    await page.fill('[data-testid="expiry-input"]', '12/25');
    await page.fill('[data-testid="cvv-input"]', '123');

    // Complete purchase
    await page.click('[data-testid="place-order-button"]');

    // Verify order confirmation
    await expect(page.locator('[data-testid="order-confirmation"]'))
      .toContainText('Order placed successfully');
    await expect(page.locator('[data-testid="order-number"]'))
      .toBeVisible();
  });
});

Performance Testing

Load Testing

javascript
// Performance testing with Artillery.js
// artillery-config.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Warm up"
    - duration: 120
      arrivalRate: 50
      name: "Ramp up load"
    - duration: 300
      arrivalRate: 100
      name: "Sustained load"
  payload:
    path: "./test-data/users.csv"
    fields:
      - "email"
      - "password"

scenarios:
  - name: "User login and browse"
    weight: 70
    flow:
      - post:
          url: "/api/auth/login"
          json:
            email: "{{ email }}"
            password: "{{ password }}"
          capture:
            - json: "$.token"
              as: "authToken"
      - get:
          url: "/api/products"
          headers:
            Authorization: "Bearer {{ authToken }}"
      - get:
          url: "/api/products/{{ $randomInt(1, 100) }}"
          headers:
            Authorization: "Bearer {{ authToken }}"

  - name: "Product search"
    weight: 20
    flow:
      - get:
          url: "/api/products/search"
          qs:
            q: "{{ $randomString() }}"
            limit: 20

  - name: "Add to cart"
    weight: 10
    flow:
      - post:
          url: "/api/auth/login"
          json:
            email: "{{ email }}"
            password: "{{ password }}"
          capture:
            - json: "$.token"
              as: "authToken"
      - post:
          url: "/api/cart/items"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            productId: "{{ $randomInt(1, 100) }}"
            quantity: "{{ $randomInt(1, 5) }}"

Browser Performance Testing

javascript
// Performance testing with Lighthouse CI
import lighthouse from 'lighthouse';
import chromeLauncher from 'chrome-launcher';

const performanceTest = async (url) => {
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
  
  const options = {
    logLevel: 'info',
    output: 'json',
    onlyCategories: ['performance'],
    port: chrome.port,
  };
  
  const runnerResult = await lighthouse(url, options);
  
  await chrome.kill();
  
  const { lhr } = runnerResult;
  const { score } = lhr.categories.performance;
  
  return {
    performanceScore: score * 100,
    metrics: {
      firstContentfulPaint: lhr.audits['first-contentful-paint'].numericValue,
      largestContentfulPaint: lhr.audits['largest-contentful-paint'].numericValue,
      cumulativeLayoutShift: lhr.audits['cumulative-layout-shift'].numericValue,
      totalBlockingTime: lhr.audits['total-blocking-time'].numericValue,
      speedIndex: lhr.audits['speed-index'].numericValue
    }
  };
};

// Performance test suite
describe('Performance Tests', () => {
  it('should meet performance benchmarks for home page', async () => {
    const results = await performanceTest('http://localhost:3000');
    
    expect(results.performanceScore).toBeGreaterThan(90);
    expect(results.metrics.firstContentfulPaint).toBeLessThan(2000);
    expect(results.metrics.largestContentfulPaint).toBeLessThan(2500);
    expect(results.metrics.cumulativeLayoutShift).toBeLessThan(0.1);
  });
  
  it('should meet performance benchmarks for product page', async () => {
    const results = await performanceTest('http://localhost:3000/products/1');
    
    expect(results.performanceScore).toBeGreaterThan(85);
    expect(results.metrics.totalBlockingTime).toBeLessThan(300);
  });
});

Test Automation

CI/CD Integration

yaml
# GitHub Actions workflow for testing
name: Test Suite

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run unit tests
      run: npm run test:unit
    
    - name: Generate coverage report
      run: npm run test:coverage
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3

  integration-tests:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run database migrations
      run: npm run db:migrate
      env:
        DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
    
    - name: Run integration tests
      run: npm run test:integration
      env:
        DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db

  e2e-tests:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright
      run: npx playwright install
    
    - name: Build application
      run: npm run build
    
    - name: Start application
      run: npm start &
    
    - name: Wait for application
      run: npx wait-on http://localhost:3000
    
    - name: Run E2E tests
      run: npm run test:e2e
    
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: playwright-report
        path: playwright-report/

Test Data Management

javascript
// Test data factories and fixtures
class TestDataFactory {
  static createUser(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      name: faker.name.fullName(),
      email: faker.internet.email(),
      password: 'password123',
      createdAt: new Date(),
      updatedAt: new Date(),
      ...overrides
    };
  }
  
  static createProduct(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      name: faker.commerce.productName(),
      description: faker.commerce.productDescription(),
      price: parseFloat(faker.commerce.price()),
      category: faker.commerce.department(),
      inStock: faker.datatype.boolean(),
      ...overrides
    };
  }
  
  static createOrder(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      userId: faker.datatype.uuid(),
      items: [
        {
          productId: faker.datatype.uuid(),
          quantity: faker.datatype.number({ min: 1, max: 5 }),
          price: parseFloat(faker.commerce.price())
        }
      ],
      total: parseFloat(faker.commerce.price()),
      status: 'pending',
      createdAt: new Date(),
      ...overrides
    };
  }
}

// Database seeding for tests
class TestDatabaseSeeder {
  async seedUsers(count = 10) {
    const users = [];
    for (let i = 0; i < count; i++) {
      const user = TestDataFactory.createUser();
      users.push(await this.userRepository.create(user));
    }
    return users;
  }
  
  async seedProducts(count = 50) {
    const products = [];
    for (let i = 0; i < count; i++) {
      const product = TestDataFactory.createProduct();
      products.push(await this.productRepository.create(product));
    }
    return products;
  }
  
  async cleanup() {
    await this.orderRepository.deleteAll();
    await this.productRepository.deleteAll();
    await this.userRepository.deleteAll();
  }
}

Testing Best Practices

Test Organization

  • Clear Test Structure: Organize tests by feature or component
  • Descriptive Test Names: Use clear, descriptive test names
  • Test Independence: Ensure tests can run independently
  • Test Data Isolation: Use separate test data for each test

Test Quality

  • Single Responsibility: Each test should verify one behavior
  • Arrange-Act-Assert: Follow the AAA pattern
  • Test Edge Cases: Include boundary and error conditions
  • Maintainable Tests: Write tests that are easy to understand and modify

Coverage Goals

  • Unit Tests: 80%+ code coverage
  • Integration Tests: Cover critical user paths
  • E2E Tests: Cover main user journeys
  • Performance Tests: Monitor key performance metrics

This comprehensive testing guide ensures robust quality assurance for mini programs through systematic testing practices and automation.

Connecting Multiple Platforms, Empowering Innovation