SuperAPI AI Directory

writing-tests

Testing & quality

View repository

Updated 10-15 21:43

About this skill

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

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:

// ✅ 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:

// 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:

// 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):

// ✅ 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:

    // 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:

    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:

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:

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

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

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

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:

// Bad
const result = await myEffect;

✅ Use Effect.runPromise:

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

❌ Don't forget to clean up resources:

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

✅ Always clean up in afterEach:

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

❌ Don't mock when real code works:

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

✅ Use real implementations:

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

❌ Don't leave debug logging enabled:

// 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:

// 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:

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:

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:

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:

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