# Writing Tests

> Write comprehensive tests for all components using Jest, Effect-TS, and test helpers. Use when creating new tests, adding test cases, or when the user asks to write tests, add test coverage, or test specific functionality.

- Skill: `forcedotcom/writing-tests` (Agent Skill)
- Install (CLI): `npx skillmds@latest add forcedotcom/writing-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/forcedotcom/writing-tests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: forcedotcom (https://skillmd.com/u/forcedotcom)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/forcedotcom/writing-tests

---


# Writing Tests

## Core Principles

**When writing tests:**

1. Use Jest with `describe`/`it` blocks
2. Use Effect-TS for async operations (not async/await)
3. Test both valid and invalid cases
4. **Add debugging to production code** using `Effect.logDebug` in the code being tested
5. **Add debugging to test code** using `console.log` in tests
6. **Enable console logging while debugging** - Use `enableConsoleLogging()` and `setLogLevel('debug')` in `beforeEach`, then change to `setLogLevel('error')` when tests are ready
7. **Prefer real implementations over mocks** - Use real services, managers, and components unless mocking is absolutely necessary

## Test Structure

### Basic Test Template

```typescript
import { MyComponent } from '../../src/components/MyComponent';
import { ApexSymbolManager } from '../../src/symbols/ApexSymbolManager';
import { CompilerService } from '../../src/parser/compilerService';
import { Effect } from 'effect';
import { enableConsoleLogging, setLogLevel } from '@salesforce/apex-lsp-shared';

describe('MyComponent', () => {
  let symbolManager: ApexSymbolManager;
  let compilerService: CompilerService;

  beforeEach(() => {
    symbolManager = new ApexSymbolManager();
    compilerService = new CompilerService();

    // Enable console logging - set to 'debug' while debugging, 'error' when ready
    enableConsoleLogging();
    setLogLevel('error');
  });

  afterEach(() => {
    symbolManager.clear();
  });

  it('should do something correctly', async () => {
    // Test implementation
    const result = await doSomething();
    expect(result).toBe(expectedValue);
  });
});
```

## Using Real Code vs Mocks

**Prefer real implementations** - Tests should use real code whenever possible:

```typescript
// ✅ Good - Use real implementations
let symbolManager: ApexSymbolManager;
let compilerService: CompilerService;

beforeEach(() => {
  symbolManager = new ApexSymbolManager();
  compilerService = new CompilerService();
});
```

**Use mocks only when absolutely necessary:**

- External services that can't be controlled (network calls, file system in some cases)
- Code that has side effects that would break test isolation
- Performance-critical tests where real implementations are too slow
- Testing error conditions that are difficult to trigger with real code

**Avoid mocks for:**

- `ApexSymbolManager` - Use real instances
- `CompilerService` - Use real instances
- Internal services and components - Use real implementations
- Effect services - Use real `EffectTestLoggerLive` and other live implementations
- Internal types and utilities - Use real implementations

**Example of acceptable mock usage:**

```typescript
// Only mock when testing error conditions that are hard to trigger
const mockService = {
  getData: jest.fn().mockRejectedValue(new Error('Network error')),
} as unknown as MyService;
```

**Example of preferred real code:**

```typescript
// Use real symbol manager - it's fast and provides better test coverage
const symbolManager = new ApexSymbolManager();
await Effect.runPromise(symbolManager.addSymbolTable(symbolTable, uri));
```

## Effect-TS in Tests

**Always use Effect.runPromise** (not async/await with Effect):

```typescript
// ✅ Correct
await Effect.runPromise(symbolManager.resolveCrossFileReferencesForFile(uri));

// ❌ Wrong
await symbolManager.resolveCrossFileReferencesForFile(uri);
```

## Debugging Tests

**Two approaches for debugging:**

1. **Add debugging to production code** using Effect logging:

   ```typescript
   // In your production code, add Effect logging for debugging:
   yield * Effect.logDebug(`Processing ${items.length} items`);
   yield * Effect.logInfo('Operation started');
   yield * Effect.logWarning('Potential issue detected');
   yield * Effect.logError(`Operation failed: ${error.message}`);
   ```

2. **Add debugging to test code** using `console.log`:

   ```typescript
   const result = await myFunction();

   // Debug: Log results if test fails
   if (result !== expectedValue) {
     console.log('Unexpected result:', JSON.stringify(result, null, 2));
     console.log('Expected:', expectedValue);
   }

   expect(result).toBe(expectedValue);
   ```

**Enable console logging while debugging tests:**

```typescript
import { enableConsoleLogging, setLogLevel } from '@salesforce/apex-lsp-shared';

beforeEach(() => {
  symbolManager = new ApexSymbolManager();
  compilerService = new CompilerService();

  // Enable console logging and set to debug level while debugging
  enableConsoleLogging();
  setLogLevel('debug');
});
```

**When tests are ready, change log level to error:**

```typescript
beforeEach(() => {
  symbolManager = new ApexSymbolManager();
  compilerService = new CompilerService();

  // Set to error level for production-ready tests
  enableConsoleLogging();
  setLogLevel('error');
});
```

**Note:** Effect logging from production code will appear in test output when console logging is enabled and log level is set appropriately. Ensure Effect services provide `EffectTestLoggerLive` or similar logging layers.

## Common Patterns

### Testing Valid Cases

```typescript
it('should handle valid input correctly', async () => {
  const input = createValidInput();
  const result = await myFunction(input);

  expect(result).toBeDefined();
  expect(result.isValid).toBe(true);
});
```

### Testing Invalid Cases

```typescript
it('should handle invalid input correctly', async () => {
  const input = createInvalidInput();
  const result = await myFunction(input);

  expect(result.isValid).toBe(false);
  expect(result.errors).toContain('Expected error message');
});
```

### Testing with Fixtures

```typescript
import * as fs from 'fs';
import * as path from 'path';

const loadFixture = (filename: string): string => {
  const fixturePath = path.join(__dirname, '../fixtures', filename);
  return fs.readFileSync(fixturePath, 'utf8');
};

it('should process fixture correctly', async () => {
  const content = loadFixture('MyFixture.cls');
  const result = await processContent(content);

  expect(result).toBeDefined();
});
```

## Test File Naming

Test files follow the pattern: `{ComponentName}.test.ts`

Examples:

- `OperatorValidator.test.ts`
- `ApexSymbolManager.getSymbolAtPosition.test.ts`
- `SymbolReference.test.ts`
- `ApexSymbolCollectorListener.assignment.test.ts`

## Anti-Patterns to Avoid

**❌ Don't use async/await with Effect:**

```typescript
// Bad
const result = await myEffect;
```

**✅ Use Effect.runPromise:**

```typescript
// Good
const result = await Effect.runPromise(myEffect);
```

**❌ Don't forget to clean up resources:**

```typescript
// Bad - missing afterEach
beforeEach(() => {
  symbolManager = new ApexSymbolManager();
});
```

**✅ Always clean up in afterEach:**

```typescript
// Good
afterEach(() => {
  symbolManager.clear();
});
```

**❌ Don't mock when real code works:**

```typescript
// Bad - unnecessary mock
const mockSymbolManager = {
  getSymbol: jest.fn().mockReturnValue(symbol),
} as ApexSymbolManager;
```

**✅ Use real implementations:**

```typescript
// Good - real code provides better test coverage
const symbolManager = new ApexSymbolManager();
await Effect.runPromise(symbolManager.addSymbolTable(symbolTable, uri));
```

**❌ Don't leave debug logging enabled:**

```typescript
// Bad - debug level left in production-ready test
beforeEach(() => {
  enableConsoleLogging();
  setLogLevel('debug'); // Should be 'error' when ready
});
```

**✅ Set log level to error when tests are ready:**

```typescript
// Good - error level for production-ready tests
beforeEach(() => {
  enableConsoleLogging();
  setLogLevel('error');
});
```

## Validator-Specific Testing

For validator tests, use helpers from `validation-test-helpers.ts`:

### compileFixtureWithOptions

Compiles a fixture file and creates validation options:

```typescript
import {
  compileFixtureWithOptions,
  runValidator,
} from './helpers/validation-test-helpers';

const { symbolTable, options } = await compileFixtureWithOptions(
  VALIDATOR_CATEGORY, // e.g., 'operator', 'variable-resolution'
  'MyFixture.cls', // Filename in test/fixtures/validation/{category}/
  undefined, // Optional file URI
  symbolManager,
  compilerService,
  {
    tier: ValidationTier.IMMEDIATE, // or ValidationTier.THOROUGH
    allowArtifactLoading: false, // true for TIER 2 tests
  },
);
```

### runValidator

Runs a validator Effect with all required services:

```typescript
const result = await runValidator(
  MyValidator.validate(symbolTable, options),
  symbolManager,
);
```

**Returns:** `ValidationResult` with `isValid`, `errors`, and `warnings` arrays.

**Note:** `runValidator` automatically provides `EffectTestLoggerLive`, so validators can use Effect logging (`Effect.logDebug`, `Effect.logInfo`, etc.). To see Effect logs in test output, enable console logging with `enableConsoleLogging()` and set an appropriate log level with `setLogLevel()`.

### Testing TIER 2 (THOROUGH) Validation

TIER 2 tests require cross-file resolution:

```typescript
describe('TIER 2: Cross-file type resolution', () => {
  it('should validate with resolved types', async () => {
    // Compile dependent files first
    await compileFixtureWithOptions(
      VALIDATOR_CATEGORY,
      'DependentClass.cls',
      undefined,
      symbolManager,
      compilerService,
      {
        tier: ValidationTier.THOROUGH,
        allowArtifactLoading: true,
      },
    );

    // Compile the file under test
    const { symbolTable, options } = await compileFixtureWithOptions(
      VALIDATOR_CATEGORY,
      'TestClass.cls',
      undefined,
      symbolManager,
      compilerService,
      {
        tier: ValidationTier.THOROUGH,
        allowArtifactLoading: true,
      },
    );

    // Resolve cross-file references
    await Effect.runPromise(
      symbolManager.resolveCrossFileReferencesForFile(
        symbolTable.getFileUri() || '',
      ),
    );

    const result = await runValidator(
      MyValidator.validate(symbolTable, options),
      symbolManager,
    );

    expect(result.isValid).toBe(true);
    expect(result.errors).toHaveLength(0);
  });
});
```

### Testing Error Codes

For validator tests, check for specific error codes:

```typescript
import { ErrorCodes } from '../../../../src/generated/ErrorCodes';

const hasError = result.errors.some(
  (e: any) => e.code === ErrorCodes.INVALID_COMPARISON_TYPES,
);
expect(hasError).toBe(true);
```

## Additional Resources

- **Test Helpers**: See `test/semantics/validation/validators/helpers/validation-test-helpers.ts` for validator-specific helpers
- **Error Codes**: See `src/generated/ErrorCodes.ts` for validator error codes
- **Example Tests**:
  - Validators: `test/semantics/validation/validators/OperatorValidator.test.ts`
  - Symbol Manager: `test/symbols/ApexSymbolManager.getSymbolAtPosition.test.ts`
  - Types: `test/types/typeReference.test.ts`
- **Effect-TS Testing**: See `prefer-effect-ts` skill for Effect patterns

