# 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:**
```bash
# 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.go` → `server/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:**
```go
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:**
```typescript
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();
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:**
```typescript
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:**
```typescript
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:**
```go
// 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:**
```typescript
// Factory functions
function createTestUser(overrides?: Partial): 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:**
```bash
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):**
```typescript
it('should handle async operation', async () => {
const result = await asyncFunction();
expect(result).toBe('expected');
});
```
**Error Testing:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
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:**
```typescript
// # 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:**
```typescript
/**
* @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*