CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tslint

An extensible static analysis linter for the TypeScript language

Pending
Overview
Eval results
Files

testing.mddocs/

Testing

TSLint provides a comprehensive testing framework for validating custom rules with markup-based test files and automated test runners.

Testing Framework Overview

TSLint's testing system uses a markup format that allows you to specify expected lint failures directly in test source code using comments.

Core Testing Functions

import { Test } from 'tslint';

// Run tests in a directory
function runTest(testDirectory: string, rulesDirectory?: string | string[]): TestResult

// Run tests matching patterns  
function runTests(patterns: string[], rulesDirectory?: string | string[]): TestResult[]

// Handle single test result output
function consoleTestResultHandler(testResult: TestResult, logger: Logger): void

// Handle multiple test results output
function consoleTestResultsHandler(testResults: TestResult[], logger: Logger): void

Test Result Interfaces

interface Logger {
    log(message: string): void;
    error(message: string): void;
}

interface TestResult {
    directory: string;
    results: {
        [fileName: string]: TestOutput | SkippedTest;
    };
}

interface TestOutput {
    skipped: false;
    errorsFromLinter: LintError[];
    errorsFromMarkup: LintError[];
    fixesFromLinter: string;
    fixesFromMarkup: string;
    markupFromLinter: string;
    markupFromMarkup: string;
}

interface SkippedTest {
    skipped: true;
    requirement: string;
}

interface LintError {
    endPos: {
        col: number;
        line: number;
    };
    message: string;
    startPos: {
        col: number;
        line: number;
    };
}

Test File Structure

Test Directory Layout

test/
├── rules/
│   ├── my-rule/
│   │   ├── test.ts.lint         # Test file with markup
│   │   ├── test.ts.fix          # Expected fix result (optional)
│   │   └── tslint.json          # Rule configuration
│   └── another-rule/
│       ├── valid.ts.lint
│       ├── invalid.ts.lint
│       └── tslint.json
└── custom-rules/                # Custom rules directory
    ├── myRuleRule.ts
    └── anotherRuleRule.ts

Test Configuration (tslint.json)

{
    "rules": {
        "my-rule": [true, "option1", "option2"],
        "semicolon": true
    }
}

Test Markup Format

Basic Markup Syntax

Test files use special comments to indicate expected failures:

// In test.ts.lint file
function example() {
    console.log('test');  
    ~~~~~~~~~~~~~~~       [error message here]
}

let unused = 5;
    ~~~~~~             [Variable 'unused' is never used]

const noSemicolon = true
                       ~ [Missing semicolon]

Markup Rules:

  • ~ characters mark the span where an error is expected
  • [message] specifies the expected error message
  • Line must align with the code position
  • Multiple errors per line are supported

Advanced Markup Features

Multi-line Errors

function longFunction(
    parameter1: string,
    ~~~~~~~~~~~~~~~~~~~ [Parameter should be on single line]
    parameter2: number
    ~~~~~~~~~~~~~~~~~~
) {
    return parameter1 + parameter2;
}

Multiple Errors on Same Line

let a = 1, b = 2;
    ~     [Variable 'a' should use const]
           ~     [Variable 'b' should use const]

Error Message Escaping

const message = 'Don\'t use quotes';
                ~~~~~~~~~~~~~~~~~ [Don't use single quotes inside strings]

// Escape brackets in messages
const test = `template ${variable}`;
                       ~~~~~~~~~~~ [Prefer 'string' over \`template\`]

Fix Testing

For rules that provide auto-fixes, create a .fix file with the expected result:

test.ts.lint:

const value = "double";
              ~~~~~~~~ [Use single quotes]

test.ts.fix:

const value = 'double';

Running Tests

Command Line Testing

# Run all tests in test directory
tslint --test test/rules/

# Run specific rule tests
tslint --test test/rules/my-rule/

# Run tests with custom rules directory
tslint --test test/rules/ --rules-dir custom-rules/

Programmatic Testing

import { Test } from 'tslint';

// Run single test directory
const testResult = Test.runTest('test/rules/my-rule', 'custom-rules');

// Handle results
Test.consoleTestResultHandler(testResult, console);

// Check if tests passed
const passed = Object.values(testResult.results).every(result => {
    if (result.skipped) return true;
    return result.errorsFromLinter.length === 0;
});

console.log(`Tests ${passed ? 'PASSED' : 'FAILED'}`);

Batch Testing

import { Test } from 'tslint';
import * as glob from 'glob';

// Run multiple test directories
const testDirs = glob.sync('test/rules/*');
const testResults = Test.runTests(testDirs, 'custom-rules');

// Process all results
Test.consoleTestResultsHandler(testResults, console);

// Generate summary
const summary = testResults.reduce((acc, result) => {
    Object.values(result.results).forEach(test => {
        if (test.skipped) {
            acc.skipped++;
        } else if (test.errorsFromLinter.length > 0) {
            acc.failed++;
        } else {
            acc.passed++;
        }
    });
    return acc;
}, { passed: 0, failed: 0, skipped: 0 });

console.log(`Summary: ${summary.passed} passed, ${summary.failed} failed, ${summary.skipped} skipped`);

Advanced Testing Patterns

Testing Rule Options

// test/rules/my-rule/default-options.ts.lint
console.log('test');
~~~~~~~~~~~~~~~~~~~ [Default message]

// test/rules/my-rule/custom-options.ts.lint  
console.log('test');
~~~~~~~~~~~~~~~~~~~ [Custom message with option]

// test/rules/my-rule/tslint.json
{
    "rules": {
        "my-rule": [true, { "customMessage": "Custom message with option" }]
    }
}

Testing Type-Aware Rules

Type-aware rules require a tsconfig.json in the test directory:

test/rules/my-typed-rule/
├── test.ts.lint
├── tslint.json
├── tsconfig.json
└── helper.ts          # Additional files for type checking

tsconfig.json:

{
    "compilerOptions": {
        "target": "es2017",
        "module": "commonjs",
        "strict": true
    },
    "files": [
        "test.ts",
        "helper.ts"
    ]
}

test.ts.lint:

import { Helper } from './helper';

const helper: Helper = new Helper();
const result: string = helper.getValue(); // Type info available
                       ~~~~~~~~~~~~~~~ [Expected number, got string]

Testing JavaScript Rules

Test JavaScript-specific rules by using .js.lint extension:

test/rules/my-js-rule/
├── test.js.lint
└── tslint.json

tslint.json:

{
    "jsRules": {
        "my-js-rule": true
    }
}

Conditional Testing

Skip tests based on conditions using special markup:

// test-conditional.ts.lint
/* tslint:disable-file */ // Skip entire file

// Or skip with reason
/// <reference path="./skipThis.d.ts" />  
/* tslint:disable */
// This test is skipped because TypeScript version < 3.0
/* tslint:enable */

Custom Test Runners

Building a Test Runner

import { Test, TestResult } from 'tslint';
import * as fs from 'fs';
import * as path from 'path';

class CustomTestRunner {
    private rulesDirectory: string;
    
    constructor(rulesDirectory: string) {
        this.rulesDirectory = rulesDirectory;
    }
    
    async runAllTests(): Promise<boolean> {
        const testDirs = this.findTestDirectories();
        const results: TestResult[] = [];
        
        for (const testDir of testDirs) {
            const result = Test.runTest(testDir, this.rulesDirectory);
            results.push(result);
        }
        
        return this.processResults(results);
    }
    
    private findTestDirectories(): string[] {
        const testRoot = './test/rules';
        
        return fs.readdirSync(testRoot)
            .map(dir => path.join(testRoot, dir))
            .filter(dir => fs.statSync(dir).isDirectory());
    }
    
    private processResults(results: TestResult[]): boolean {
        let allPassed = true;
        
        results.forEach(result => {
            console.log(`\nTesting ${result.directory}:`);
            
            Object.entries(result.results).forEach(([fileName, testOutput]) => {
                if (testOutput.skipped) {
                    console.log(`  ${fileName}: SKIPPED (${testOutput.reason})`);
                } else {
                    const errors = testOutput.errorsFromLinter;
                    if (errors.length === 0) {
                        console.log(`  ${fileName}: PASSED`);
                    } else {
                        console.log(`  ${fileName}: FAILED`);
                        errors.forEach(error => {
                            console.log(`    ${error.message} at ${error.startPos.line}:${error.startPos.col}`);
                        });
                        allPassed = false;
                    }
                }
            });
        });
        
        return allPassed;
    }
}

// Usage
const runner = new CustomTestRunner('./custom-rules');
runner.runAllTests().then(passed => {
    process.exit(passed ? 0 : 1);
});

Continuous Integration Testing

import { Test } from 'tslint';
import * as fs from 'fs';

interface CITestResult {
    success: boolean;
    summary: {
        total: number;
        passed: number;
        failed: number;
        skipped: number;
    };
    failures: Array<{
        rule: string;
        file: string;
        errors: string[];
    }>;
}

function runCITests(): CITestResult {
    const testResults = Test.runTests(['test/rules/*'], 'src/rules');
    
    const result: CITestResult = {
        success: true,
        summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
        failures: []
    };
    
    testResults.forEach(testResult => {
        const ruleName = path.basename(testResult.directory);
        
        Object.entries(testResult.results).forEach(([fileName, output]) => {
            result.summary.total++;
            
            if (output.skipped) {
                result.summary.skipped++;
            } else if (output.errorsFromLinter.length > 0) {
                result.summary.failed++;
                result.success = false;
                
                result.failures.push({
                    rule: ruleName,
                    file: fileName,
                    errors: output.errorsFromLinter.map(e => e.message)
                });
            } else {
                result.summary.passed++;
            }
        });
    });
    
    return result;
}

// Generate CI report
const ciResult = runCITests();
fs.writeFileSync('test-results.json', JSON.stringify(ciResult, null, 2));

if (!ciResult.success) {
    console.error('Tests failed!');
    process.exit(1);
}

Test Coverage Analysis

import { Test, TestResult } from 'tslint';
import * as fs from 'fs';
import * as glob from 'glob';

interface CoverageReport {
    rulesCovered: string[];
    rulesUntested: string[];
    coveragePercentage: number;
    testStats: {
        totalTests: number;
        passingTests: number;
        failingTests: number;
    };
}

function analyzeCoverage(): CoverageReport {
    // Find all available rules
    const ruleFiles = glob.sync('src/rules/*Rule.ts');
    const availableRules = ruleFiles.map(file => 
        path.basename(file, 'Rule.ts').replace(/([A-Z])/g, '-$1').toLowerCase().substring(1)
    );
    
    // Find tested rules
    const testDirs = glob.sync('test/rules/*');
    const testedRules = testDirs.map(dir => path.basename(dir));
    
    // Run tests to get statistics
    const testResults = Test.runTests(testDirs, 'src/rules');
    
    const testStats = testResults.reduce((stats, result) => {
        Object.values(result.results).forEach(output => {
            stats.totalTests++;
            if (!output.skipped) {
                if (output.errorsFromLinter.length === 0) {
                    stats.passingTests++;
                } else {
                    stats.failingTests++;
                }
            }
        });
        return stats;
    }, { totalTests: 0, passingTests: 0, failingTests: 0 });
    
    const rulesUntested = availableRules.filter(rule => !testedRules.includes(rule));
    
    return {
        rulesCovered: testedRules,
        rulesUntested,
        coveragePercentage: Math.round((testedRules.length / availableRules.length) * 100),
        testStats
    };
}

// Generate coverage report
const coverage = analyzeCoverage();
console.log(`Rule Coverage: ${coverage.coveragePercentage}% (${coverage.rulesCovered.length}/${coverage.rulesCovered.length + coverage.rulesUntested.length})`);
console.log(`Untested rules: ${coverage.rulesUntested.join(', ')}`);

Best Practices

Test Development Guidelines

  1. Comprehensive Coverage: Test all code paths and edge cases
  2. Clear Test Names: Use descriptive file names for different scenarios
  3. Proper Markup: Ensure markup exactly matches expected error positions
  4. Test Fixes: Always test auto-fix functionality when applicable
  5. Type-aware Testing: Use TypeScript projects for type-dependent rules
  6. Edge Cases: Test boundary conditions and invalid inputs
  7. Configuration Testing: Test different rule option combinations

Test Organization

test/
├── rules/
│   ├── rule-name/
│   │   ├── basic/
│   │   │   ├── test.ts.lint
│   │   │   └── tslint.json
│   │   ├── with-options/
│   │   │   ├── test.ts.lint  
│   │   │   └── tslint.json
│   │   ├── edge-cases/
│   │   │   ├── empty-file.ts.lint
│   │   │   ├── large-file.ts.lint
│   │   │   └── tslint.json
│   │   └── fixes/
│   │       ├── test.ts.lint
│   │       ├── test.ts.fix
│   │       └── tslint.json

Debugging Failed Tests

import { Test } from 'tslint';

function debugFailedTest(testPath: string) {
    const result = Test.runTest(testPath, 'custom-rules');
    
    Object.entries(result.results).forEach(([fileName, output]) => {
        if (!output.skipped && output.errorsFromLinter.length > 0) {
            console.log(`\nDebugging ${fileName}:`);
            console.log('Expected errors from markup:');
            output.errorsFromMarkup.forEach(error => {
                console.log(`  ${error.startPos.line}:${error.startPos.col} - ${error.message}`);
            });
            
            console.log('Actual errors from linter:');  
            output.errorsFromLinter.forEach(error => {
                console.log(`  ${error.startPos.line}:${error.startPos.col} - ${error.message}`);
            });
            
            console.log('Differences:');
            // Compare and show differences
            const expectedMessages = new Set(output.errorsFromMarkup.map(e => e.message));
            const actualMessages = new Set(output.errorsFromLinter.map(e => e.message));
            
            expectedMessages.forEach(msg => {
                if (!actualMessages.has(msg)) {
                    console.log(`  Missing: ${msg}`);
                }
            });
            
            actualMessages.forEach(msg => {
                if (!expectedMessages.has(msg)) {
                    console.log(`  Unexpected: ${msg}`);
                }
            });
        }
    });
}

Install with Tessl CLI

npx tessl i tessl/npm-tslint

docs

cli.md

configuration.md

formatters.md

index.md

linting.md

rules.md

testing.md

tile.json