mattermost/.planning/codebase/TESTING.md
Nick Misasi 52659ea589 docs: map existing codebase
- STACK.md - Technologies and dependencies
- ARCHITECTURE.md - System design and patterns
- STRUCTURE.md - Directory layout
- CONVENTIONS.md - Code style and patterns
- TESTING.md - Test structure
- INTEGRATIONS.md - External services
- CONCERNS.md - Technical debt and issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:15:51 -05:00

8.5 KiB

Testing Patterns

Analysis Date: 2026-01-13

Test Framework

Backend (Go):

  • Runner: Go testing package (built-in)
  • Config: Standard Go test conventions
  • Run: make test or go test ./...

Frontend (Jest):

  • Runner: Jest 30.1.3
  • Config: webapp/channels/jest.config.js
  • Assertion: Jest built-in expect
  • Timeout: 60000ms

E2E (Playwright):

  • Runner: Playwright 1.57.0
  • Config: e2e-tests/playwright/playwright.config.ts
  • Visual: Percy integration (PERCY_TOKEN)

Run Commands:

# Backend
make test                              # Run all Go tests
go test ./server/channels/app/...      # Run specific package
go test -v -run TestFunctionName       # Run specific test

# Frontend
npm test                               # Run all Jest tests
npm test -- --watch                    # Watch mode
npm test -- path/to/file.test.ts       # Single file
npm run test:coverage                  # Coverage report

# E2E
npm run test --prefix e2e-tests/playwright    # Run Playwright tests
npx playwright test specs/functional/         # Run functional tests

Test File Organization

Backend (Go):

  • Location: *_test.go alongside source files
  • Pattern: Same directory as implementation
  • Example: server/channels/app/user.goserver/channels/app/user_test.go

Frontend (Jest):

  • Location: *.test.ts(x) alongside source files
  • Alternative: __tests__/ subdirectories for some modules
  • Pattern: Component tests co-located with components

E2E (Playwright):

  • Location: e2e-tests/playwright/specs/
  • Organization by type:
    • specs/functional/ - Feature tests
    • specs/accessibility/ - A11y tests
    • specs/visual/ - Visual regression tests
    • specs/client/ - API client tests

Structure:

server/
  channels/
    app/
      user.go
      user_test.go
    store/
      sqlstore/
        user_store.go
        user_store_test.go

webapp/
  channels/
    src/
      components/
        about_build_modal/
          about_build_modal.tsx
          about_build_modal.test.tsx
      actions/
        channel_actions.ts
        channel_actions.test.ts
      hooks/
        useBurnOnReadTimer.ts
        useBurnOnReadTimer.test.ts

e2e-tests/
  playwright/
    specs/
      functional/
        channels/
          keyboard_shortcuts/
            shift_up_shortcut.spec.ts
      accessibility/
        channels/
          account_menu_keyboard.spec.ts

Test Structure

Go Test Structure:

func TestFunctionName(t *testing.T) {
    // Setup
    th := Setup(t)
    defer th.TearDown()

    // Test cases
    t.Run("success case", func(t *testing.T) {
        result, err := th.App.FunctionName(input)
        require.NoError(t, err)
        assert.Equal(t, expected, result)
    })

    t.Run("error case", func(t *testing.T) {
        _, err := th.App.FunctionName(invalidInput)
        require.Error(t, err)
    })
}

Jest Test Structure:

import {renderWithContext} from 'tests/react_testing_utils';

describe('ComponentName', () => {
    beforeEach(() => {
        // Reset state
    });

    it('should handle valid input', async () => {
        // Arrange
        const props = createTestProps();

        // Act
        const {getByRole} = renderWithContext(<Component {...props} />);
        await userEvent.click(getByRole('button'));

        // Assert
        expect(getByRole('dialog')).toBeVisible();
    });

    it('should throw on invalid input', () => {
        expect(() => functionCall(null)).toThrow('Invalid input');
    });
});

Playwright Test Structure:

import {test, expect} from '@e2e-support/test_fixture';

test.describe('Feature Name', () => {
    /**
     * @objective Verify that user can perform action
     * @precondition User is logged in
     */
    test('should complete action successfully', async ({pw, page}) => {
        // # Navigate to page
        await page.goto('/channels/town-square');

        // # Perform action
        await page.getByRole('button', {name: 'Submit'}).click();

        // * Verify result
        await expect(page.getByText('Success')).toBeVisible();
    });
});

Patterns:

  • Use beforeEach for per-test setup
  • Use afterEach to restore mocks
  • Explicit arrange/act/assert comments in complex tests
  • One assertion focus per test (multiple expects OK)

Mocking

Go Mocking:

  • Use interfaces for testability
  • Mock stores via interface implementations
  • TestHelper struct for common setup

Jest Mocking:

import {vi} from 'vitest';

// Mock module
vi.mock('@mattermost/client', () => ({
    Client4: {
        getUser: vi.fn(),
    },
}));

// Mock in test
const mockGetUser = vi.mocked(Client4.getUser);
mockGetUser.mockResolvedValue({id: 'user-id', username: 'testuser'});

What to Mock:

  • External APIs and services
  • File system operations
  • Network calls
  • Time/dates (use fake timers)
  • Redux store (via renderWithContext)

What NOT to Mock:

  • Internal pure functions
  • Simple utilities
  • TypeScript types

Fixtures and Factories

Go Test Data:

// TestHelper provides test utilities
func Setup(t *testing.T) *TestHelper {
    th := &TestHelper{}
    th.App = setupApp(t)
    th.BasicUser = th.CreateUser()
    th.BasicTeam = th.CreateTeam()
    th.BasicChannel = th.CreateChannel()
    return th
}

Jest Test Data:

// Factory functions
function createTestUser(overrides?: Partial<UserProfile>): UserProfile {
    return {
        id: 'test-id',
        username: 'testuser',
        email: 'test@example.com',
        ...overrides,
    };
}

// TestHelper for channel mocks
import {TestHelper} from 'tests/helpers/client-test-helper';
const channel = TestHelper.getChannelMock();

Location:

  • Go: Test helpers in *_test.go or shared testlib/ package
  • Jest: Factory functions in test file or tests/helpers/
  • Playwright: Page objects in lib/src/ui/pages/

Coverage

Requirements:

  • No enforced coverage target
  • Focus on critical paths (API, store, business logic)
  • Coverage tracked for awareness

Configuration:

  • Jest: collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}']
  • Exclusions: Test files, config files

View Coverage:

npm run test:coverage
open coverage/index.html

Test Types

Unit Tests:

  • Scope: Single function/component in isolation
  • Mocking: Mock all external dependencies
  • Speed: Fast (<100ms per test)
  • Examples: server/channels/app/*_test.go, webapp/channels/src/**/*.test.tsx

Integration Tests:

  • Scope: Multiple modules together
  • Mocking: Mock only external boundaries
  • Examples: Store tests with real DB, Redux action tests

E2E Tests:

  • Framework: Playwright (primary), Cypress (legacy)
  • Scope: Full user flows
  • Location: e2e-tests/playwright/specs/
  • Tags: @visual for visual tests

Common Patterns

Async Testing (Jest):

it('should handle async operation', async () => {
    const result = await asyncFunction();
    expect(result).toBe('expected');
});

Error Testing:

// Sync error
it('should throw on invalid input', () => {
    expect(() => parse(null)).toThrow('Cannot parse null');
});

// Async error
it('should reject on failure', async () => {
    await expect(asyncCall()).rejects.toThrow('Error message');
});

React Testing Library Selectors:

// Prefer accessible selectors (in order)
getByRole('button', {name: 'Submit'})   // Best
getByText('Submit')                      // Good
getByPlaceholderText('Enter name')       // Good
getByLabelText('Username')               // Good
getByTestId('submit-button')             // Last resort

User Interactions:

import userEvent from '@testing-library/user-event';

// Always await userEvent methods
await userEvent.click(button);
await userEvent.type(input, 'text');
await userEvent.keyboard('{Enter}');

Playwright Comment Conventions:

// # Action - describes test step
await page.click('button');

// * Verification - describes assertion
await expect(element).toBeVisible();

Snapshot Testing:

  • Not used (deprecated pattern)
  • Prefer explicit expect().toBeVisible() assertions

Test Documentation (Playwright)

Required Format:

/**
 * @objective Clear description of what the test verifies
 * @precondition (optional) Special setup requirements
 */
test('MM-T1234 descriptive test name', async ({pw}) => {
    // Test implementation
});

Naming:

  • Action-oriented titles
  • Optional Jira key prefix: MM-T1234
  • Descriptive of expected behavior

Testing analysis: 2026-01-13 Update when test patterns change