Table of contents
Open Table of contents
Adding some basic features
Ok, we’ve got our very first version ready, let’s add some basic features. Let’s think about what features are missing compared to other libraries:
-
Most test libraries will automatically check which files should be tested before execution, we need such a feature to reduce our users’ workload.
-
Most test libraries provide a
CLI
to process jobs in shell, so we need to get one. -
Most test libraries allow users to hook into the lifecycle of tests to avoid repeating setup and teardown code, this is also important for our library.
-
The first version of our library doesn’t have the ability to transform users’ code, so they can only write javascript code while using our library( we need
TypeScript
support! ).
So let’s build our new library step by step.
Automatically check which files should be under test
Like Jest, we can have a naming convention that all test files should be formatted as xxx.spec.js
or xxx.test.js
, with this we can efficiently search our test files with glob pattern
.
// import a library called `globby`, which provides a bunch of useful features
// for matching glob patterns.
import { globby } from 'globby'
// prodive an internal `getTestFiles` for automatically check which files
// should be tested before execution.
const getTestFiles = async () => {
const paths = await globby(['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}'], {
// ignore dependencies , artifacts and config files
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
],
absolute: true,
})
return paths
}
And then we don’t need users to provide their own getTestFiles
to handle that job.
Command Line Interface
This step may be easier than you think, as Node.js
provides a useful interface
to help implement this process.
Create a new directory called bin
at the root of your project, then add code below:
#!/usr/bin/env node
The code above is called hashbang
, and adds this line to tell Node.js
that
it is an executable script.
And then import the getTestFiles
function that we declared earlier:
#!/usr/bin/env node
import { getTestFiles, runner } from '../index'
// This line of code will be the part of the code that takes effect.
runner(getTestFiles)
In your package.json
, add the following code:
{
"bin": {
"tinytest": "./dist/bin/index.js"
}
}
where tinytest
is the name of your cli, which you can change to whatever you like,
followed by the build path, which you need to change to your own.
Hooks
With the power of EventEmitter
, we can easily get the job done.
Create a new file called hook.ts
, then add the following code:
import { EventEmitter } from 'events'
const hookCenter = new EventEmitter()
const beforeEach = (callback: () => void) => {
hookCenter.on('before-each', callback)
}
const afterEach = (callback: () => void) => {
hookCenter.on('after-each', callback)
}
export { hookCenter, beforeEach, afterEach }
In your register
file, emit the events added in above hooks:
const it = (name: string, callback: (ctx: unknown) => void) => {
hookCenter.emit('before-each')
try {
callback(context)
} catch (r: unknown) {
//@ts-ignore
const reportee = r as IReportee
if (reportee.passed) {
console.log(chalk.green(`${name} passed`))
} else {
console.warn(chalk.red(`${name} failed`))
console.warn(
chalk.green(`Expected: ${JSON.stringify(reportee.expected)}`)
)
console.warn(chalk.red(`Received: ${JSON.stringify(reportee.received)}`))
}
}
hookCenter.emit('after-each')
}
TypeScript support
Why do we even need this section to transform code written in TypeScript
?
Because Nodejs
doesn’t provide a way to run it natively, the transformation process
before execution is essential.
We choose esbuild to help us finish this job, if you
don’t know what esbuild
is yet, please check this link out first.
import * as esbuild from 'esbuild'
import { name as thisModuleName } from '../package.json'
// This is the path where you will put the transformed files
import { outputFolderPath } from './shared'
const transform = async (entrys: string[]) => {
await esbuild.build({
// original paths of test files
entryPoints: entrys,
outdir: outputFolderPath,
bundle: true,
// do not include this module (our testing library)
external: [thisModuleName],
platform: 'node',
format: 'esm',
outExtension: {
'.js': '.mjs',
},
})
}
export { transform }
Then add the transform
function to our runner
:
// other code...
await transform(testFiles)
const filenames = await readdir(outputFolderPath, {
withFileTypes: true,
encoding: 'utf-8',
})
// other code...
Congratulations! We’ve done it all!