Skip to main content

Test Suite Overview

GitHub Desktop uses Node.js’s built-in test runner for unit tests. Tests are organized under app/test/ with the following structure:
  • unit/ - Unit tests for individual modules and functions
  • integration/ - End-to-end tests using UI automation
  • fixtures/ - Git repositories and test data
  • helpers/ - Shared test utilities and setup code

Running Tests

Run All Tests

yarn test
This runs all unit tests by default (equivalent to yarn test:unit).

Unit Tests

Run all unit tests:
yarn test:unit

Specific Test Files

Run tests in a specific file:
yarn test:unit app/test/unit/git/commit-test.ts

Specific Test Directory

Run all tests in a directory:
yarn test:unit app/test/unit/git

Filter by Test Name

Run tests matching a specific pattern:
yarn test:unit --test-name-pattern "parse commit"
For more test runner options, see the Node.js test runner documentation.

ESLint Tests

Run tests for custom ESLint rules:
yarn test:eslint

Script Tests

Run tests for build scripts:
yarn test:script

Writing Unit Tests

Creating a New Test Module

1

Create Test File

Create a file named [module-name]-test.ts in the appropriate directory under app/test/unit/:
describe('module being tested', () => {
  it('can test some code', () => {
    expect(true).toBe(true)
  })
})
2

Run the Test

Verify your test file is recognized:
yarn test:unit app/test/unit/your-module-test.ts
3

Write Real Tests

Replace the placeholder test with actual test cases for your module.

Test Structure

Follow the Arrange-Act-Assert pattern:
import { parseCommit } from '../../../src/lib/git/commit'

describe('parseCommit', () => {
  it('parses a simple commit message', () => {
    // Arrange - Set up test data
    const message = 'Fix bug in parser'
    
    // Act - Execute the function
    const result = parseCommit(message)
    
    // Assert - Check the results
    expect(result.summary).toBe('Fix bug in parser')
    expect(result.description).toBeNull()
  })
  
  it('parses commit with description', () => {
    const message = 'Fix bug in parser\n\nThis fixes issue #123'
    const result = parseCommit(message)
    
    expect(result.summary).toBe('Fix bug in parser')
    expect(result.description).toBe('This fixes issue #123')
  })
})

Best Practices

Keep tests focused: Each test should verify one specific behavior or scenario.
  • Test module organization: Match the directory structure of app/src/
  • Descriptive test names: Use clear, readable descriptions
  • Test edge cases: Cover error conditions and boundary cases
  • Avoid complexity: If tests are complex, consider refactoring the code
  • No code comments needed: Tests should be self-documenting

Example: Testing Pure Functions

import { updateChangedFiles } from '../../../src/lib/stores/updates/changes-state'

describe('updateChangedFiles', () => {
  it('preserves selection when files are unchanged', () => {
    const state = {
      workingDirectory: createTestWorkingDirectory(),
      selectedFileIDs: ['file1.ts'],
      diff: null
    }
    const status = createTestStatus()
    
    const result = updateChangedFiles(state, status, false)
    
    expect(result.selectedFileIDs).toEqual(['file1.ts'])
  })
  
  it('clears selection when files are removed', () => {
    const state = {
      workingDirectory: createTestWorkingDirectory(['file1.ts']),
      selectedFileIDs: ['file1.ts'],
      diff: null
    }
    const status = createTestStatus([]) // No files
    
    const result = updateChangedFiles(state, status, false)
    
    expect(result.selectedFileIDs).toEqual([])
  })
})

Testing State Updates

When testing complex state logic in AppStore:
  1. Extract logic to pure functions outside of AppStore
  2. Take current state as a parameter instead of reading implicit state
  3. Return new state rather than mutating existing state
  4. Test the extracted function in isolation

Example Pattern

// ✅ Good: Pure function that's easy to test
export function updateChangedFiles(
  state: IChangesState,
  status: IStatusResult,
  clearPartialState: boolean
): ChangedFilesResult {
  // Pure logic here
  return {
    workingDirectory: newWorkingDirectory,
    selectedFileIDs: newSelection,
    diff: newDiff
  }
}

// In AppStore:
this.repositoryStateCache.updateChangesState(repository, state =>
  updateChangedFiles(state, status, clearPartialState)
)
See app/src/lib/stores/updates/changes-state.ts for a complete example.

Test Setup

Run test setup scripts:
yarn test:setup
This initializes test fixtures and prepares the test environment.

Continuous Integration

All tests run automatically on pull requests. Ensure your tests pass locally before pushing:
yarn test
Pull requests must pass all tests before they can be merged.

Test Fixtures

Test fixtures are located in app/test/fixtures/ and include:
  • Sample Git repositories
  • Test data files
  • Mock configurations
When adding tests that require Git repositories, reuse existing fixtures or create new ones in this directory.

Debugging Tests

To debug a failing test:
  1. Run the specific test with the file path
  2. Add console.log statements to inspect values
  3. Use Node.js debugger with --inspect flag
  4. Check test isolation - ensure tests don’t depend on each other

Next Steps

Build docs developers (and LLMs) love