Test in TypeScript
The integration of TypeScript (TS) with popular JavaScript (JS) testing frameworks is streamlined through tools that handle the necessary compilation and source mapping, allowing developers to write tests in TS while leveraging established JS ecosystems. Testing and debugging strategies largely revolve around two core techniques: transpiling TS to JS and using source maps, or running TS directly using tools like ts-node
.
Why Checking and Testing is Necessary
The need for rigorous checks is fundamentally driven by the nature of traditional JavaScript. JavaScript is an interpreted language that requires the code to be executed (run) to determine its validity. If errors are present, a developer might complete the code only to receive no output or unexpected behaviour, necessitating potentially hours spent tracking down bugs because issues are typically surfaced only at runtime.
JavaScript’s freedom and dynamic typing, while flexible, become a disadvantage when safety is required, as the language avoids exceptions wherever possible instead of alerting the developer to invalid operations. This delay between making a mistake and discovering it often when the program is already running or deployed is the reason TypeScript was created.
The Role of TypeScript in Error Checking
TypeScript is often described as JavaScript plus an optional static type system. Its primary motivation is to introduce static typing, enabling developers to catch errors at compile time rather than having to wait for them to crash the system at runtime.
This compile-time error checking, provided by the TypeScript transpiler, highlights errors before the script is run. The type checker verifies that the code is type-safe, catching issues such as calling a method on an object when that method doesn’t exist, or calling a function with an argument of the wrong type.
The compiler catching errors is preferable to having things fail at runtime. Furthermore, TypeScript provides instant feedback, displaying error messages directly in the text editor as the code is typed.
Benefits of Static Analysis
Moving error detection earlier into the development cycle yields significant advantages, effectively reducing the reliance on purely runtime unit tests to catch basic type errors:
- Safety and Reliability: The type system makes programs safer by preventing them from performing invalid operations, eliminating entire classes of type-related bugs. Static typing also makes code easier to debug and maintain compared to standard JavaScript.
- Code Quality and Documentation: Types enhance code quality and documentation. Explicitly declaring types makes the codebase self-descriptive, making it easier for developers to understand the expected data types for function parameters and returns.
- Refactoring and Tooling: TypeScript allows editors and tools to perform automated refactors that are type-aware, enhancing developer productivity. By enforcing usage restrictions, TypeScript ensures that changes in one area do not break other parts of the code that rely on it.
Testing with TypeScript and JavaScript Frameworks
You can utilise TypeScript code with any JavaScript testing framework, and before you run tests, you can always do a basic TS-to-JS transform. But some technologies improve the development experience by doing the translation automatically.
Jest Configuration ()
Jest is a popular framework for unit testing with good TypeScript support. Ts-jest
, a TypeScript preprocessor for Jest, is the main tool for using Jest with TypeScript.
Installation
Installing Jest, the TypeScript definitions for Jest, and the ts-jest
preprocessor are typically required for the setup as development dependencies:
npm i jest @types/jest ts-jest typescript -D
Debugging and correct reporting against the original .ts
code are made possible by ts-jest
, which plays a vital role in enabling Jest to transpile TypeScript files on the fly with ease and by providing built-in source map support.
Jest Configuration Details
Jest needs to be configured; this is usually done directly in package.json
or through a jest.config.js
file. All TypeScript files should typically be kept in a src
folder for a tidy project setup.
A simple configuration setup could resemble this (using the package.json
or jest.config.js
compatible format):
// package.json snippet demonstrating Jest configuration
{
"jest": {
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
An explanation of the main options
"roots"
: Specifies the directory Jest should search for files (e.g., assuming files are insrc/
)."transform"
: Instructs Jest to usets-jest
to handle files ending in.ts
or.tsx
. This is howts-jest
integrates to perform on-the-fly transpilation. In older Jest versions, the transform value might point directly to<rootDir>/node_modules/ts-jest/preprocessor.js
."testRegex"
: Defines the naming conventions Jest uses to locate test files (e.g., files in__tests__
folders, or files ending in.test
or.spec
).
Jest Example (Code and Output)
Examine a straightforward utility function in src/foo.ts
:
// src/foo.ts
export const sum
= (...a: number[]) =>
a.reduce((acc, val) => acc + val, 0);
And the corresponding test file in src/foo.test.ts
:
// src/foo.test.ts
import { sum } from '../src/foo';
test('basic sum test', () => {
expect(sum()).toBe(0);
});
test('basic again', () => {
expect(sum(1, 2)).toBe(3);
});
To run the tests, you execute npx jest
or npm test
(if configured with "test": "jest"
in package.json
).
Example Output:
PASS ./foo.test.ts
✓ basic sum test (3ms)
✓ basic again (3ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.46s, estimated 2s
Ran all test suites.
Jest also supports running tests with code coverage generation using jest --coverage
and requires an optional configuration line for TypeScript coverage reporting: "testResultsProcessor": "<rootDir>/node_modules/ts-jest/coverageprocessor.js"
.
Testing with tape
The TAP-compliant markup output of the simple JavaScript testing framework Tape is well-known. With TypeScript, it is simple to use by utilising ts-node
for execution.
Installation and Usage
When using tape
, the framework and associated types must be installed, along with the ts-node
globally:
npm install --save-dev tape @types/tape
npm install -g ts-node
Example test in math.test.ts
:
// math.test.ts
import * as test from "tape";
test("Math test", (t) => {
t.equal(4, 2 + 2); // assertion 1
t.true(5 > 2 + 2); // assertion 2
t.end();
});
The test is executed directly using ts-node
:
ts-node node_modules/tape/bin/tape math.test.ts
Example Output:
TAP version 13
# Math test
ok 1 should be equal
ok 2 should be truthy
1..2
# tests 2
# pass 2
# ok
Testing with Cypress
Cypress is a well-liked End-to-End (E2E) testing tool. Cypress has the major benefit of coming pre-configured with TypeScript definitions.
When setting up Cypress for a TypeScript project, it is recommended to create a separate e2e
directory. This strategy helps avoid dependency conflicts and prevents the testing framework from polluting the global namespace (with functions like describe
, it
, and expect
), which could cause conflicts with the project’s main tsconfig.json
or node_modules
.
Cypress also provides a beneficial interactive Google Chrome debug experience. Debugging Cypress tests can involve using a debugger
statement in application code, or utilizing the .debug()
command provided by Cypress in test code to pause execution.