Table of contents
Open Table of contents
No longer a black box
The title hints at what follows: yes, we need to build a library ourselves to enable us to understand how it works.
There are several questions we need to know first:
- How to run test files ?
- How to register a callback to handle the AAA (
Arrange
,Act
,Assert
) ? - How to report test results to users?
If you don’t know how to answer yet, don’t worry, you’ll get the hang of it in the following sections.
Making a simple runner
What does runner do? Get the files and run, that’s it, there is no magic here.
import { fork } from 'child_process'
// a naive runner without involving parallelism
const runner = async (getTestFiles: () => Promise<string[]>) => {
// here we use a callback function to inject the implementation
const testFiles = await getTestFiles()
// iterate all the files
for(const testFile of testFiles) {
// here is the key to the runner, we forked a nodejs process
// to run our test file
const cmd = fork(testFile)
cmd.on('exit', code => process.exit(code ?? 0))
cmd.on('error', err => {
throw err
})
}
}
That’s all there is to runner, easy, right?
AAA register
We don’t need to know how users will use it, but its interface, it will look like this:
const it = (testSuiteName: string, callback: () => void) => {
callback()
}
However, if this is all it is, it has no way of making assertions,
because it doesn’t do anything more than call the callback
function.
An assert helper
We need a function to help us handle assertions, like:
const expect = <T = unknown>(value: T, another: T, comparator: Function) => {
}
Slightly cumbersome and not close enough to natural language, if we want a Jest
-like
chained call in our assert helper, how should we do?
Simply return an object with a comparator function as its property:
const expect = <T = unkown>(value: T) => {
return {
toBe: (another: T) => {
// some comparisons
}
}
}
With the above implementation, we can easily make chain calls: expect(1 + 1).toBe(2)
.
Report messages to users
We are very close to the first available version of our library and only need to implement a reporter, let’s refactor the code above.
const expect = <T = unknown>(value: T) => {
return {
toBe: (another: T) => {
if(Object.is(value, another)) {
throw {
passed: true
}
}else {
throw {
passed: false
}
}
}
}
}
and then, in our register:
const it = (name: string, callback: () => void) => {
try{
callback()
}catch(r: unknown) {
const passed: boolean = r.passed
if(passed) {
console.log(`${name} passed`)
}else{
console.warn(`${name} failed`)
}
}
}
We are done! Let’s test it out.
Create two files in your workspace, one named calc.mjs
, the other named calc.test.mjs
// calc.mjs
export const calc = (m, n) => m + n
// calc.test.mjs
import { it, expect } from './tiny-testing-lib'
import { calc } from './calc.mjs'
it('calculate one plus one equals two', () => {
expect(calc(1, 1)).toBe(2)
})
Set up our runner:
// main.mjs
import { runner } from './tiny-testing-lib'
import { join } from 'path'
const getTestFiles = () => {
const file = join(process.cwd(), './calc.test.mjs')
return Promise.resolve([file])
}
runner(getTestFiles)
Set up scripts in package.json
:
{
"scripts": {
"test": "node ./main.mjs"
}
}
Then type npm run test
into your terminal , you will see the magic!
calculate one plus one equals two passed