Testing Unit Testing - hiraishikentaro/rails-factorybot-jump GitHub Wiki
Unit testing in Rails FactoryBot Jump focuses on testing individual components in isolation, ensuring each piece of functionality works correctly without dependencies on external systems like the file system or VSCode APIs.
Mocha Configuration (src/test/suite/index.ts
):
import * as path from "path";
import * as Mocha from "mocha";
import * as glob from "glob";
export function run(): Promise<void> {
const mocha = new Mocha({
ui: "tdd",
color: true,
timeout: 10000,
});
const testsRoot = path.resolve(__dirname, "..");
return new Promise((c, e) => {
glob("**/**.test.js", { cwd: testsRoot }, (err, files) => {
if (err) return e(err);
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
e(err);
}
});
});
}
Source: src/test/suite/index.ts
Test File Structure (src/test/suite/extension.test.ts
):
import * as assert from "assert";
import * as vscode from "vscode";
import * as sinon from "sinon";
import { FactoryLinkProvider } from "../../providers/factoryLinkProvider";
suite("Extension Test Suite", () => {
vscode.window.showInformationMessage("Start all tests.");
// Core functionality tests
// Configuration tests
// Error handling tests
// Performance tests
});
Source: src/test/suite/extension.test.ts
Pattern Recognition Testing:
suite("Factory Detection", () => {
let provider: FactoryLinkProvider;
setup(() => {
provider = new FactoryLinkProvider();
});
test("detects basic factory calls", () => {
const text = "user = create(:user)";
const regex = /(?:create|build)\s*\(\s*:([a-zA-Z0-9_]+)/g;
const matches = Array.from(text.matchAll(regex));
assert.strictEqual(matches.length, 1);
assert.strictEqual(matches[0][1], "user");
});
test("detects factory calls without parentheses", () => {
const text = "user = create :user";
const regex = /(?:create|build)\s+:([a-zA-Z0-9_]+)/g;
const matches = Array.from(text.matchAll(regex));
assert.strictEqual(matches.length, 1);
assert.strictEqual(matches[0][1], "user");
});
test("detects multiple factory methods", () => {
const testCases = [
"create(:user)",
"build(:user)",
"create_list(:user, 5)",
"build_list(:user, 3)",
"build_stubbed(:user)",
"build_stubbed_list(:user, 2)",
];
const regex =
/(?:create|create_list|build|build_list|build_stubbed|build_stubbed_list)\s*(?:\(\s*)?:([a-zA-Z0-9_]+)/g;
testCases.forEach((testCase) => {
const matches = Array.from(testCase.matchAll(regex));
assert.strictEqual(matches.length, 1, `Failed for: ${testCase}`);
assert.strictEqual(matches[0][1], "user", `Failed for: ${testCase}`);
});
});
test("detects factory calls with traits", () => {
const text = "admin = create(:user, :admin, :verified)";
const regex =
/(?:create|build)\s*\(\s*:([a-zA-Z0-9_]+)(?:\s*,\s*:([a-zA-Z0-9_]+))*/g;
const match = regex.exec(text);
assert.ok(match);
assert.strictEqual(match[1], "user");
// Test trait extraction
const traitRegex = /:([a-zA-Z0-9_]+)/g;
const traits = Array.from(text.matchAll(traitRegex)).map((m) => m[1]);
assert.deepStrictEqual(traits, ["user", "admin", "verified"]);
});
test("ignores factory calls in comments", () => {
const text = "# user = create(:user)\nreal_user = create(:user)";
const lines = text.split("\n");
const nonCommentLines = lines.filter(
(line) => !line.trim().startsWith("#")
);
assert.strictEqual(nonCommentLines.length, 1);
assert.ok(nonCommentLines[0].includes("real_user"));
});
});
Cache Operations Testing:
suite("Cache Management", () => {
let provider: FactoryLinkProvider;
let mockWorkspace: sinon.SinonStub;
let mockFindFiles: sinon.SinonStub;
let mockReadFile: sinon.SinonStub;
setup(() => {
provider = new FactoryLinkProvider();
// Mock VSCode workspace API
mockWorkspace = sinon.stub(vscode.workspace, "findFiles");
mockReadFile = sinon.stub();
});
teardown(() => {
sinon.restore();
});
test("builds empty cache with no factory files", async () => {
mockWorkspace.resolves([]);
await provider.initializeFactoryFiles();
const cache = provider.getFactoryCache();
assert.strictEqual(cache.size, 0);
});
test("builds factory cache from file content", async () => {
const factoryFileUri = vscode.Uri.file("/spec/factories/users.rb");
const factoryContent = `
FactoryBot.define do
factory :user do
name { "Test User" }
end
factory :admin_user do
name { "Admin User" }
end
end`;
mockWorkspace.resolves([factoryFileUri]);
mockReadFile.withArgs(factoryFileUri).resolves(factoryContent);
// Mock file reading
sinon.stub(vscode.workspace, "openTextDocument").resolves({
getText: () => factoryContent,
} as any);
await provider.initializeFactoryFiles();
const cache = provider.getFactoryCache();
assert.ok(cache.has("user"));
assert.ok(cache.has("admin_user"));
assert.strictEqual(cache.get("user")?.uri.path, "/spec/factories/users.rb");
});
test("builds trait cache from factory content", async () => {
const factoryContent = `
FactoryBot.define do
factory :user do
name { "Test User" }
trait :admin do
admin { true }
end
trait :verified do
verified { true }
end
end
end`;
const factoryFileUri = vscode.Uri.file("/spec/factories/users.rb");
mockWorkspace.resolves([factoryFileUri]);
sinon.stub(vscode.workspace, "openTextDocument").resolves({
getText: () => factoryContent,
} as any);
await provider.initializeFactoryFiles();
const traitCache = provider.getTraitCache();
assert.ok(traitCache.has("user:admin"));
assert.ok(traitCache.has("user:verified"));
assert.strictEqual(traitCache.get("user:admin")?.factory, "user");
});
test("handles cache updates correctly", async () => {
const initialContent = `
FactoryBot.define do
factory :user do
name { "Test User" }
end
end`;
const updatedContent = `
FactoryBot.define do
factory :user do
name { "Test User" }
end
factory :post do
title { "Test Post" }
end
end`;
const factoryFileUri = vscode.Uri.file("/spec/factories/users.rb");
mockWorkspace.resolves([factoryFileUri]);
const mockDocument = sinon.stub();
mockDocument.onFirstCall().resolves({ getText: () => initialContent });
mockDocument.onSecondCall().resolves({ getText: () => updatedContent });
sinon.stub(vscode.workspace, "openTextDocument").callsFake(mockDocument);
// Initial cache build
await provider.initializeFactoryFiles();
let cache = provider.getFactoryCache();
assert.ok(cache.has("user"));
assert.ok(!cache.has("post"));
// Update cache
await provider.initializeFactoryFiles();
cache = provider.getFactoryCache();
assert.ok(cache.has("user"));
assert.ok(cache.has("post"));
});
});
Document Link Creation Testing:
suite("Link Generation", () => {
let provider: FactoryLinkProvider;
let mockDocument: any;
setup(() => {
provider = new FactoryLinkProvider();
mockDocument = {
uri: vscode.Uri.file("/spec/models/user_spec.rb"),
getText: () => "user = create(:user)",
lineAt: sinon.stub(),
positionAt: sinon.stub().returns(new vscode.Position(0, 0)),
};
});
test("creates document links for factory calls", async () => {
// Setup cache with factory definition
const factoryUri = vscode.Uri.file("/spec/factories/users.rb");
provider.setFactoryCache(
new Map([["user", { uri: factoryUri, lineNumber: 1 }]])
);
const links = provider.provideDocumentLinks(mockDocument);
assert.strictEqual(links.length, 1);
assert.ok(links[0].target);
assert.ok(links[0].target.toString().includes("gotoLine"));
});
test("creates links for factory calls with traits", async () => {
mockDocument.getText = () => "admin = create(:user, :admin)";
provider.setFactoryCache(
new Map([
[
"user",
{ uri: vscode.Uri.file("/spec/factories/users.rb"), lineNumber: 1 },
],
])
);
provider.setTraitCache(
new Map([
[
"user:admin",
{
uri: vscode.Uri.file("/spec/factories/users.rb"),
lineNumber: 5,
factory: "user",
},
],
])
);
const links = provider.provideDocumentLinks(mockDocument);
// Should create links for both factory and trait
assert.strictEqual(links.length, 2);
});
test("returns empty array for unknown factories", async () => {
mockDocument.getText = () => "user = create(:unknown_factory)";
provider.setFactoryCache(
new Map([
[
"user",
{ uri: vscode.Uri.file("/spec/factories/users.rb"), lineNumber: 1 },
],
])
);
const links = provider.provideDocumentLinks(mockDocument);
assert.strictEqual(links.length, 0);
});
test("handles multiple factory calls in same document", async () => {
mockDocument.getText = () => `
user = create(:user)
post = create(:post)
comment = build(:comment)
`;
provider.setFactoryCache(
new Map([
[
"user",
{ uri: vscode.Uri.file("/spec/factories/users.rb"), lineNumber: 1 },
],
[
"post",
{ uri: vscode.Uri.file("/spec/factories/posts.rb"), lineNumber: 1 },
],
[
"comment",
{
uri: vscode.Uri.file("/spec/factories/comments.rb"),
lineNumber: 1,
},
],
])
);
const links = provider.provideDocumentLinks(mockDocument);
assert.strictEqual(links.length, 3);
});
});
Settings Handling Testing:
suite("Configuration", () => {
let mockConfiguration: any;
setup(() => {
mockConfiguration = {
get: sinon.stub(),
};
sinon.stub(vscode.workspace, "getConfiguration").returns(mockConfiguration);
});
teardown(() => {
sinon.restore();
});
test("uses default factory paths when no configuration", () => {
mockConfiguration.get.withArgs("factoryPaths").returns(undefined);
const provider = new FactoryLinkProvider();
const paths = provider.getFactoryPaths();
assert.deepStrictEqual(paths, ["spec/factories/**/*.rb"]);
});
test("uses custom factory paths from configuration", () => {
const customPaths = ["test/factories/**/*.rb", "lib/factories/**/*.rb"];
mockConfiguration.get.withArgs("factoryPaths").returns(customPaths);
const provider = new FactoryLinkProvider();
const paths = provider.getFactoryPaths();
assert.deepStrictEqual(paths, customPaths);
});
test("handles invalid configuration gracefully", () => {
mockConfiguration.get.withArgs("factoryPaths").returns("invalid-string");
const provider = new FactoryLinkProvider();
const paths = provider.getFactoryPaths();
// Should fall back to default
assert.deepStrictEqual(paths, ["spec/factories/**/*.rb"]);
});
});
Robust Error Handling Testing:
suite("Error Handling", () => {
let provider: FactoryLinkProvider;
setup(() => {
provider = new FactoryLinkProvider();
});
test("handles file reading errors gracefully", async () => {
const mockWorkspace = sinon.stub(vscode.workspace, "findFiles");
mockWorkspace.resolves([vscode.Uri.file("/spec/factories/users.rb")]);
const mockOpenDocument = sinon.stub(vscode.workspace, "openTextDocument");
mockOpenDocument.rejects(new Error("File not found"));
// Should not throw
await assert.doesNotReject(async () => {
await provider.initializeFactoryFiles();
});
const cache = provider.getFactoryCache();
assert.strictEqual(cache.size, 0);
sinon.restore();
});
test("handles malformed factory files", async () => {
const malformedContent = `
This is not valid Ruby syntax
factory :user
end without begin
`;
const factoryFileUri = vscode.Uri.file("/spec/factories/users.rb");
sinon.stub(vscode.workspace, "findFiles").resolves([factoryFileUri]);
sinon.stub(vscode.workspace, "openTextDocument").resolves({
getText: () => malformedContent,
} as any);
// Should not throw and should continue processing
await assert.doesNotReject(async () => {
await provider.initializeFactoryFiles();
});
sinon.restore();
});
test("handles empty factory files", async () => {
const emptyContent = "";
const factoryFileUri = vscode.Uri.file("/spec/factories/users.rb");
sinon.stub(vscode.workspace, "findFiles").resolves([factoryFileUri]);
sinon.stub(vscode.workspace, "openTextDocument").resolves({
getText: () => emptyContent,
} as any);
await provider.initializeFactoryFiles();
const cache = provider.getFactoryCache();
assert.strictEqual(cache.size, 0);
sinon.restore();
});
});
class MockFactoryBuilder {
private content = "FactoryBot.define do\n";
addFactory(name: string, traits: string[] = []): this {
this.content += ` factory :${name} do\n`;
this.content += ` name { "Test ${name}" }\n`;
traits.forEach((trait) => {
this.content += ` trait :${trait} do\n`;
this.content += ` ${trait} { true }\n`;
this.content += ` end\n`;
});
this.content += ` end\n\n`;
return this;
}
build(): string {
return this.content + "end";
}
}
// Usage in tests
const factoryContent = new MockFactoryBuilder()
.addFactory("user", ["admin", "verified"])
.addFactory("post", ["published"])
.build();
class MockDocumentBuilder {
private lines: string[] = [];
addLine(content: string): this {
this.lines.push(content);
return this;
}
addFactoryCall(method: string, factory: string, traits: string[] = []): this {
const traitString =
traits.length > 0 ? `, ${traits.map((t) => `:${t}`).join(", ")}` : "";
this.addLine(`${factory}_instance = ${method}(:${factory}${traitString})`);
return this;
}
build(): any {
const content = this.lines.join("\n");
return {
uri: vscode.Uri.file("/test/spec.rb"),
getText: () => content,
lineAt: sinon.stub(),
positionAt: sinon.stub().returns(new vscode.Position(0, 0)),
};
}
}
// Usage in tests
const testDocument = new MockDocumentBuilder()
.addFactoryCall("create", "user", ["admin"])
.addFactoryCall("build", "post")
.build();
suite("Performance", () => {
test("cache initialization performance", async () => {
const provider = new FactoryLinkProvider();
// Create mock factory files
const factoryFiles = Array.from({ length: 100 }, (_, i) =>
vscode.Uri.file(`/spec/factories/factory_${i}.rb`)
);
const factoryContent = new MockFactoryBuilder()
.addFactory("user", ["admin"])
.addFactory("post", ["published"])
.build();
sinon.stub(vscode.workspace, "findFiles").resolves(factoryFiles);
sinon.stub(vscode.workspace, "openTextDocument").resolves({
getText: () => factoryContent,
} as any);
const startTime = Date.now();
await provider.initializeFactoryFiles();
const duration = Date.now() - startTime;
assert.ok(
duration < 5000,
`Cache initialization took ${duration}ms, expected < 5000ms`
);
sinon.restore();
});
test("link generation performance", () => {
const provider = new FactoryLinkProvider();
// Large test document
const largeDocument = new MockDocumentBuilder();
for (let i = 0; i < 1000; i++) {
largeDocument.addFactoryCall("create", "user");
}
const document = largeDocument.build();
// Setup cache
provider.setFactoryCache(
new Map([
[
"user",
{ uri: vscode.Uri.file("/spec/factories/users.rb"), lineNumber: 1 },
],
])
);
const startTime = Date.now();
const links = provider.provideDocumentLinks(document);
const duration = Date.now() - startTime;
assert.strictEqual(links.length, 1000);
assert.ok(
duration < 1000,
`Link generation took ${duration}ms, expected < 1000ms`
);
});
});
# Run all tests
npm test
# Run tests in watch mode (if configured)
npm run test:watch
# Run with coverage (if configured)
npm run test:coverage
# Run only unit tests (excluding integration)
npm run test:unit
Successful Test Run:
Extension Test Suite
Factory Detection
✓ detects basic factory calls (2ms)
✓ detects factory calls without parentheses (1ms)
✓ detects multiple factory methods (5ms)
✓ detects factory calls with traits (3ms)
✓ ignores factory calls in comments (1ms)
Cache Management
✓ builds empty cache with no factory files (15ms)
✓ builds factory cache from file content (25ms)
✓ builds trait cache from factory content (20ms)
✓ handles cache updates correctly (30ms)
27 passing (145ms)
This comprehensive unit testing approach ensures each component of the Rails FactoryBot Jump extension works correctly in isolation, providing confidence in the overall system reliability and maintainability.