Testing Testing Strategy - hiraishikentaro/rails-factorybot-jump GitHub Wiki
Testing: Testing Strategy
Testing Philosophy
Rails FactoryBot Jump follows a comprehensive testing strategy that emphasizes reliability, maintainability, and realistic testing environments. The strategy balances unit testing for isolated components with integration testing for VSCode API interactions.
Testing Framework Architecture
Core Testing Stack
Primary Framework: Mocha 10.8.2
- JavaScript testing framework with flexible test organization
- Support for asynchronous testing
- Rich assertion library integration
- Custom timeout handling for VSCode operations
VSCode Testing Environment: @vscode/test-electron 2.4.1
- Runs tests in actual VSCode Extension Development Host
- Full VSCode API access during testing
- Realistic testing environment with file system integration
- Cross-platform testing support
Mocking Framework: Sinon 17.0.1
- Comprehensive mocking and stubbing capabilities
- VSCode API mocking for isolated unit tests
- File system operation mocking
- Call verification and behavior testing
Test Runner Architecture
Dual Test Runner System:
The extension implements separate test runners for different testing needs:
Unit Test Runner (src/test/runUnitTests.ts
):
- Focused on isolated component testing
- Fast execution without VSCode environment overhead
- Comprehensive mocking of external dependencies
- Suitable for TDD workflows
Integration Test Runner (src/test/runTest.ts
):
- Full VSCode Extension Development Host environment
- Real API interactions and file system operations
- End-to-end workflow testing
- Realistic user scenario simulation
Source: package.json#L78,L84,L88
Test Environment Configuration
Test Runner Setup (src/test/runTest.ts
):
import { runTests } from "@vscode/test-electron";
async function main() {
const extensionDevelopmentPath = path.resolve(__dirname, "../../");
const extensionTestsPath = path.resolve(__dirname, "./suite/index");
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
});
}
Benefits:
- Real VSCode environment testing
- Actual extension activation and deactivation
- True file system operations
- Authentic user interaction simulation
Source: src/test/runTest.ts
Testing Levels
1. Unit Testing
Scope: Individual components and functions in isolation
Target Components:
- Models: Factory, Location, and Trait data models
- Services: CacheManager, FactoryParser, FileSearcher, ConfigurationManager, ErrorNotificationService
- Utils: RegexPatterns, PathUtils
- Constants: Default configuration values
Test Structure (src/test/unit/
):
unit/
├── index.ts # Unit test suite configuration
├── models/ # Model unit tests
│ ├── factory.test.ts
│ ├── location.test.ts
│ └── trait.test.ts
├── services/ # Service unit tests
│ ├── cacheManager.test.ts
│ └── factoryParser.test.ts
└── utils/ # Utility unit tests
└── regexPatterns.test.ts
Mocking Strategy:
// Example: Mock VSCode workspace API
const mockWorkspace = {
findFiles: sinon.stub(),
getConfiguration: sinon.stub(),
createFileSystemWatcher: sinon.stub(),
};
// Example: Mock file system operations
const mockFs = {
readFile: sinon.stub(),
stat: sinon.stub(),
};
Isolation Benefits:
- Fast test execution
- Predictable test conditions
- Easy debugging of specific logic
- Clear failure attribution
2. Integration Testing
Scope: Component interactions with VSCode APIs and file system
Integration Points:
- Document link provider registration
- File system watcher functionality
- Configuration change handling
- Command execution workflow
Real Environment Testing:
// Example: Test with real VSCode document
const document = await vscode.workspace.openTextDocument(testFileUri);
const provider = new FactoryLinkProvider();
const links = provider.provideDocumentLinks(document);
Integration Benefits:
- Realistic API behavior testing
- Cross-component interaction verification
- End-to-end workflow validation
- Platform-specific behavior testing
3. System Testing
Scope: Complete extension functionality in realistic scenarios
Test Scenarios:
- Extension activation and deactivation
- Factory file discovery and parsing
- Link generation and navigation
- Configuration updates and cache refresh
User Workflow Simulation:
// Simulate complete user workflow
await activateExtension();
await openRubyTestFile();
await hoverOverFactoryCall();
await clickFactoryLink();
await verifyNavigationToFactory();
Test Categories
1. Core Functionality Tests
Factory Detection Tests:
suite("Factory Detection", () => {
test("detects basic factory calls", () => {
const text = "user = create(:user)";
const matches = detectFactoryCalls(text);
assert.strictEqual(matches.length, 1);
assert.strictEqual(matches[0].factoryName, "user");
});
test("detects factory calls with traits", () => {
const text = "admin = create(:user, :admin, :verified)";
const matches = detectFactoryCalls(text);
assert.strictEqual(matches[0].factoryName, "user");
assert.deepStrictEqual(matches[0].traits, ["admin", "verified"]);
});
});
Cache Management Tests:
suite("Cache Management", () => {
test("builds factory cache from files", async () => {
const provider = new FactoryLinkProvider();
await provider.initializeFactoryFiles();
const cache = provider.getFactoryCache();
assert.ok(cache.has("user"));
assert.ok(cache.has("post"));
});
test("updates cache on file changes", async () => {
// Test cache invalidation and rebuild
});
});
2. Configuration Tests
Settings Handling Tests:
suite("Configuration", () => {
test("uses default factory paths", () => {
const config = getDefaultConfiguration();
assert.deepStrictEqual(config.factoryPaths, ["spec/factories/**/*.rb"]);
});
test("respects custom factory paths", async () => {
await updateConfiguration({
factoryPaths: ["test/factories/**/*.rb", "lib/factories/**/*.rb"],
});
const provider = new FactoryLinkProvider();
const paths = provider.getFactoryPaths();
assert.strictEqual(paths.length, 2);
});
});
3. Edge Case Tests
Error Handling Tests:
suite("Error Handling", () => {
test("handles missing factory files gracefully", async () => {
// Mock file system to return empty results
const provider = new FactoryLinkProvider();
await provider.initializeFactoryFiles();
// Should not throw errors
});
test("handles malformed factory files", async () => {
// Test with invalid Ruby syntax
});
test("handles permission errors", async () => {
// Mock file system permission errors
});
});
Performance Tests:
suite("Performance", () => {
test("handles large numbers of factory files", async () => {
// Test with 100+ factory files
const startTime = Date.now();
await provider.initializeFactoryFiles();
const duration = Date.now() - startTime;
assert.ok(
duration < 5000,
"Initialization should complete within 5 seconds"
);
});
test("provides links efficiently for large documents", () => {
// Test with large test files
});
});
Test Data Management
1. Mock Factory Files
Test Factory Definitions:
# test/fixtures/factories/users.rb
FactoryBot.define do
factory :user do
name { "Test User" }
email { "[email protected]" }
trait :admin do
admin { true }
end
trait :verified do
verified { true }
end
end
end
Test File Management:
// Create temporary test files
const testFactoryFile = await createTestFile(
"factories/users.rb",
factoryContent
);
const testSpecFile = await createTestFile("spec/user_spec.rb", specContent);
// Cleanup after tests
after(() => {
cleanupTestFiles();
});
2. Mock VSCode Environment
Document Mocking:
const mockDocument = {
uri: vscode.Uri.file("/test/spec/user_spec.rb"),
getText: () => "user = create(:user, :admin)",
lineAt: sinon.stub(),
positionAt: sinon.stub(),
};
Configuration Mocking:
const mockConfiguration = {
get: sinon
.stub()
.withArgs("factoryPaths")
.returns(["spec/factories/**/*.rb"]),
};
Continuous Integration Testing
1. Multi-Platform Testing
Platform Matrix:
- Ubuntu Latest: Linux compatibility testing
- macOS Latest: macOS compatibility testing
- Windows Latest: Windows compatibility testing
Platform-Specific Concerns:
- Path separator handling (POSIX vs Windows)
- File permission behaviors
- VSCode API platform differences
2. Node.js Version Testing
Supported Versions:
- Node.js 22.x (current testing version)
- Node.js 18.x (minimum supported LTS)
- Future Node.js versions (compatibility testing)
Version-Specific Testing:
- ES2020 feature availability
- API compatibility
- Performance characteristics
3. Automated Test Execution
Separate GitHub Actions Workflows:
Unit Test Workflow (.github/workflows/unit_test.yml
):
jobs:
unit-test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run test:unit
Integration Test Workflow (.github/workflows/integration_test.yml
):
jobs:
integration-test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: xvfb-run -a npm run test:integration
if: runner.os == 'Linux'
- run: npm run test:integration
if: runner.os != 'Linux'
Test Maintenance Strategy
1. Test Organization
Suite Structure:
test/
├── runTest.ts # Integration test runner
├── runUnitTests.ts # Unit test runner
├── suite/ # Integration tests
│ ├── index.ts # Integration test configuration
│ └── extension.test.ts # Integration test cases
├── unit/ # Unit tests
│ ├── index.ts # Unit test configuration
│ ├── models/ # Model unit tests
│ ├── services/ # Service unit tests
│ ├── utils/ # Utility unit tests
│ └── unitTestsRunner.ts # Unit test execution logic
└── fixtures/ # Test data files
2. Test Quality Metrics
Coverage Goals:
- Core functionality: 95%+ coverage
- Error handling paths: 80%+ coverage
- Configuration scenarios: 90%+ coverage
Quality Indicators:
- Unit tests: under 10 seconds execution
- Integration tests: under 30 seconds execution
- Zero flaky tests
- Clear test failure messages
- Minimal test interdependencies
Test Execution Commands:
npm test
- Run integration tests (default)npm run test:unit
- Run unit tests onlynpm run test:integration
- Run integration tests onlynpm run test:all
- Run both unit and integration testsnpm run test:watch
- Run tests in watch mode
3. Regression Testing
Change Impact Testing:
- New feature tests
- Backward compatibility tests
- Performance regression tests
Automated Regression Detection:
- Baseline performance metrics
- API contract verification
- User workflow preservation
Testing Best Practices
1. Test Independence
Isolation Principles:
- Each test can run independently
- No shared mutable state between tests
- Clean setup and teardown for each test
2. Descriptive Test Names
Naming Convention:
test("should detect factory calls with multiple traits in parentheses syntax");
test("should handle missing factory files without throwing errors");
test("should update cache when factory file is modified");
3. Behavior-Driven Testing
Test Structure:
test("should navigate to correct factory definition", async () => {
// Given: A test file with factory call
const testFile = createTestFile("user = create(:user)");
// When: User clicks on factory link
const links = provider.provideDocumentLinks(testFile);
const factoryLink = links[0];
// Then: Should navigate to factory definition
assert.strictEqual(factoryLink.target.path, "/factories/users.rb");
});
This comprehensive testing strategy ensures the extension maintains high quality, reliability, and performance across all supported platforms and use cases.