Skip to content

Instantly share code, notes, and snippets.

@dsherret
Last active September 24, 2018 18:02
Show Gist options
  • Save dsherret/65dedf28a957d918b3a4efdf0c6ce10a to your computer and use it in GitHub Desktop.
Save dsherret/65dedf28a957d918b3a4efdf0c6ce10a to your computer and use it in GitHub Desktop.
Analyzes code to find missing tests.
/**
* Ensure Public API Has Tests
* ---------------------------
* This demonstrates analyzing code to find methods and properties from the public
* api that don't appear in the tests.
*
* This is a very basic implementation... a better implementation would examine more
* aspects of the code (ex. are the return values properly checked?) and report
* statistics about the tests that possibly indicate how they could be improved (ex.
* "this test has a lot of overlap with these other tests"). The goal would be to
* help find parts of the application that are lacking in tests (as a complement to
* code coverage) and guide developers away from writing hard to maintain tests.
*
* A stricter implementation could make sure the method is mentioned in a `describe`
* call (ex. `describe("#myMethod", ...);` which is easier to analyze when using
* ts-nameof `describe(nameof<MyClass>(c => c.myMethod), ...):`)
*
* The best implementation would probably combine static analysis with the trace
* information from running the tests.
* ---------------------------
*/
import * as path from "path";
import Project, { TypeGuards, Node, ReferenceFindableNode, Scope, ClassDeclaration, InterfaceDeclaration, Diagnostic } from "ts-simple-ast";
// config
const mainFolderPath = "path/to/project";
const entryFilePath = path.join(mainFolderPath, "src/main.ts");
const tsConfigFilePath = path.join(mainFolderPath, "tsconfig.json");
const isInTestsFolder = (filePath: string) => filePath.includes("/src/tests/");
// setup
const project = new Project({ tsConfigFilePath });
const entryFile = project.getSourceFileOrThrow(entryFilePath);
// ensure no compile errors
throwIfDiagnostics(project.getPreEmitDiagnostics());
// get public declarations
const publicDeclarations = filterClassesAndInterfaces(entryFile.getExportedDeclarations());
// get the method and properties
const methodsAndProperties = getMethodsAndProperties(publicDeclarations);
// for every method and property, check if it's referenced in the tests
for (const node of methodsAndProperties) {
const referencingNodes = node.findReferencesAsNodes();
const areTestsReferencing = referencingNodes.some(n => isInTestsFolder(n.getSourceFile().getFilePath()));
if (!areTestsReferencing) {
const filePath = node.getSourceFile().getFilePath();
const lineNumber = node.getStartLineNumber();
const message = `Node "${TypeGuards.hasName(node) ? node.getName() : node.getText()}" is not referenced in the tests.`;
console.log(`[${filePath}:${lineNumber}]: ${message}`);
}
}
function throwIfDiagnostics(diagnostics: Diagnostic[]) {
if (diagnostics.length === 0)
return;
for (const diagnostic of diagnostics)
console.error(diagnostic.getMessageText());
throw new Error("Stopping. Found compile errors!");
}
function filterClassesAndInterfaces(declarations: Node[]) {
// this basic implemention only supports class and interface declarations
const result = declarations.filter(d => TypeGuards.isClassDeclaration(d) || TypeGuards.isInterfaceDeclaration(d));
return result as (ClassDeclaration | InterfaceDeclaration)[];
}
function getMethodsAndProperties(declarations: (ClassDeclaration | InterfaceDeclaration)[]) {
const nodes: (Node & ReferenceFindableNode)[] = [];
for (const dec of declarations) {
for (const node of dec.getProperties())
addIfPublic(node);
for (const node of dec.getMethods()) {
if (TypeGuards.isMethodDeclaration(node)) {
const overloads = node.getOverloads();
if (overloads.length > 0) {
for (const overload of overloads)
addIfPublic(overload);
}
else
addIfPublic(node);
}
else
addIfPublic(node);
}
}
return nodes;
function addIfPublic(node: Node & ReferenceFindableNode) {
if (TypeGuards.isScopedNode(node) && node.getScope() !== Scope.Public)
return;
nodes.push(node);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment