Last active
September 24, 2018 18:02
-
-
Save dsherret/65dedf28a957d918b3a4efdf0c6ce10a to your computer and use it in GitHub Desktop.
Analyzes code to find missing tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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