Skip to content

Testing Standards

Testing requirements and practices.


Testing Philosophy

"Tests are not optional—they're how we validate delivery."

Every feature must be: 1. Specified in Gherkin scenarios 2. Implemented according to specs 3. Verified through automated tests 4. Traceable from task to test to code


Three-Layer Testing

Layer 1: API Tests (40%)

Test backend endpoints, validation, business logic.

@api @crud
Feature: Project API

  @smoke
  Scenario: Create project with valid data
    Given I have a valid auth token
    And I have a project payload:
      | name        | Test Project |
      | description | A test       |
    When I POST to "/projects"
    Then the response status should be 201
    And the response should contain "id"

Layer 2: UI Tests (45%)

Test frontend rendering, interactions, state.

@ui @forms
Feature: Project Form

  Scenario: Submit valid project
    Given I am on the new project page
    When I fill in "Name" with "My Project"
    And I click "Create"
    Then I should see "Project created"

Layer 3: E2E Tests (15%)

Test complete user journeys.

@e2e @critical
Feature: Project Workflow

  Scenario: Create and manage project
    Given I am logged in as "user@example.com"
    When I create a project "E-commerce"
    Then I should see it in my projects
    When I add a task "Setup database"
    Then the task should appear in the project

Tag Requirements

Required Tags (Every Scenario)

Tag Purpose
@api / @ui / @e2e Layer
@smoke / @critical / @regression Priority

Usage

# Run by layer
npm test -- --tags "@api"

# Run by priority
npm test -- --tags "@smoke"

# Run critical API tests
npm test -- --tags "@api and @critical"

Coverage Expectations

Minimum Scenarios

Feature Type API UI E2E Total
CRUD Entity 8-10 5-7 2-3 15-20
Authentication 6-8 8-10 3-4 17-22
Payment Flow 5-6 6-8 4-5 15-19

Scenario Types

Every feature needs:

  • Happy paths - Normal, successful flows
  • Error cases - Validation failures, edge cases
  • Security tests - Authorization, input validation

Writing Good Scenarios

Do

# ✅ Specific and testable
Scenario: Reject registration with existing email
  Given a user exists with email "test@example.com"
  When I register with email "test@example.com"
  Then I should see "Email already exists"
  And no new user should be created

Don't

# ❌ Vague and untestable
Scenario: Handle bad registration
  Given some users exist
  When I try to register badly
  Then it should fail

Step Definitions

Pattern

import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';

let context: any = {};

Given('I have a project payload:', function(dataTable) {
  context.payload = dataTable.rowsHash();
});

When('I POST to {string}', async function(endpoint) {
  context.response = await fetch(`${BASE_URL}${endpoint}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${context.token}`
    },
    body: JSON.stringify(context.payload)
  });
});

Then('the response status should be {int}', function(status) {
  expect(context.response.status).to.equal(status);
});

Reusability

Create reusable steps:

// Common steps
Given('I am authenticated as {string}', async function(email) {
  context.token = await login(email, 'password');
});

Given('I have a valid auth token', async function() {
  context.token = await login('test@example.com', 'password');
});

Unit Testing

Service Tests

describe('ProjectService', () => {
  let service: ProjectService;
  let mockRepo: jest.Mocked<ProjectRepository>;

  beforeEach(() => {
    mockRepo = { create: jest.fn(), findOne: jest.fn() } as any;
    service = new ProjectService(mockRepo);
  });

  describe('create', () => {
    it('should create project with valid data', async () => {
      const dto = { name: 'Test', organizationId: 'org-1' };
      mockRepo.create.mockResolvedValue({ id: '1', ...dto });

      const result = await service.create(dto);

      expect(result.id).toBe('1');
      expect(mockRepo.create).toHaveBeenCalledWith(dto);
    });

    it('should throw on duplicate name', async () => {
      mockRepo.create.mockRejectedValue(new Error('Duplicate'));

      await expect(service.create({ name: 'Existing' }))
        .rejects.toThrow('Duplicate');
    });
  });
});

Component Tests

import { render, screen, fireEvent } from '@testing-library/react';

describe('ProjectForm', () => {
  it('submits with valid data', async () => {
    const onSubmit = jest.fn();
    render(<ProjectForm onSubmit={onSubmit} />);

    fireEvent.change(screen.getByLabelText('Name'), {
      target: { value: 'My Project' }
    });
    fireEvent.click(screen.getByText('Create'));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({ name: 'My Project' });
    });
  });

  it('shows validation error for empty name', async () => {
    render(<ProjectForm onSubmit={jest.fn()} />);

    fireEvent.click(screen.getByText('Create'));

    expect(await screen.findByText('Name is required')).toBeInTheDocument();
  });
});

CI/CD Integration

GitHub Actions

name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install
        run: npm ci

      - name: Smoke tests
        run: npm test -- --tags "@smoke"

      - name: Full suite
        run: npm test

      - name: Upload report
        uses: actions/upload-artifact@v4
        with:
          name: test-report
          path: reports/

Quality Checklist

Before PR:

  • All tests pass locally
  • No skipped tests without justification
  • Coverage thresholds met (>80%)
  • New features have tests
  • Edge cases covered
  • Step definitions reusable
  • Tags applied correctly