| name | writing-tests |
| description | 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. |
Writing Tests
Core Principles
When writing tests:
- Use Jest with
describe/it blocks
- Use Effect-TS for async operations (not async/await)
- Test both valid and invalid cases
- Add debugging to production code using
Effect.logDebug in the code being tested
- Add debugging to test code using
console.log in tests
- Enable console logging while debugging - Use
enableConsoleLogging() and setLogLevel('debug') in beforeEach, then change to setLogLevel('error') when tests are ready
- 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();
enableConsoleLogging();
setLogLevel('error');
});
afterEach(() => {
symbolManager.clear();
});
it('should do something correctly', async () => {
const result = await doSomething();
expect(result).toBe(expectedValue);
});
});
Using Real Code vs Mocks
Prefer real implementations - Tests should use real code whenever possible:
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:
const mockService = {
getData: jest.fn().mockRejectedValue(new Error('Network error')),
} as unknown as MyService;
Example of preferred real code:
const symbolManager = new ApexSymbolManager();
await Effect.runPromise(symbolManager.addSymbolTable(symbolTable, uri));
Effect-TS in Tests
Always use Effect.runPromise (not async/await with Effect):
await Effect.runPromise(symbolManager.resolveCrossFileReferencesForFile(uri));
await symbolManager.resolveCrossFileReferencesForFile(uri);
Debugging Tests
Two approaches for debugging:
-
Add debugging to production code using Effect logging:
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}`);
-
Add debugging to test code using console.log:
const result = await myFunction();
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();
enableConsoleLogging();
setLogLevel('debug');
});
When tests are ready, change log level to error:
beforeEach(() => {
symbolManager = new ApexSymbolManager();
compilerService = new CompilerService();
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:
const result = await myEffect;
✅ Use Effect.runPromise:
const result = await Effect.runPromise(myEffect);
❌ Don't forget to clean up resources:
beforeEach(() => {
symbolManager = new ApexSymbolManager();
});
✅ Always clean up in afterEach:
afterEach(() => {
symbolManager.clear();
});
❌ Don't mock when real code works:
const mockSymbolManager = {
getSymbol: jest.fn().mockReturnValue(symbol),
} as ApexSymbolManager;
✅ Use real implementations:
const symbolManager = new ApexSymbolManager();
await Effect.runPromise(symbolManager.addSymbolTable(symbolTable, uri));
❌ Don't leave debug logging enabled:
beforeEach(() => {
enableConsoleLogging();
setLogLevel('debug');
});
✅ Set log level to error when tests are ready:
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,
'MyFixture.cls',
undefined,
symbolManager,
compilerService,
{
tier: ValidationTier.IMMEDIATE,
allowArtifactLoading: false,
},
);
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 () => {
await compileFixtureWithOptions(
VALIDATOR_CATEGORY,
'DependentClass.cls',
undefined,
symbolManager,
compilerService,
{
tier: ValidationTier.THOROUGH,
allowArtifactLoading: true,
},
);
const { symbolTable, options } = await compileFixtureWithOptions(
VALIDATOR_CATEGORY,
'TestClass.cls',
undefined,
symbolManager,
compilerService,
{
tier: ValidationTier.THOROUGH,
allowArtifactLoading: true,
},
);
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