Writing and Testing a stdin script with TypeScript
Laura Viglioni
Posted on May 31, 2022
Hello there, folks, how are you?
One of my motivations for writing texts here in dev.to
is to compile information which took me a lot of effort to find in one place.
So, one of these days I was writing a small application in TypeScript that used stdin from the terminal
ts-node main.ts < input.txt
Everything went well until I had to test it. I spent more time than I'd like trying to find out how I would test this input. I tried a lot of different stuff that I saw on the internet but only one of them worked and this is the solution I'll present in this text.
Code Example
First of all, an example of a script in typescript that receives stdin line by line and terminates when an empty line is entered:
// main.ts
import * as readline from 'node:readline'
import { stdin as input, stdout as output } from 'node:process'
type RL = readline.Interface
type SomeFunction = (rl: RL) => (line: string) => void
const someFunction : SomeFunction = rl => line => {
if (line === '') {
rl.close()
}
/*
* Do something with `line`
*/
const result = // doSomething(line)
console.log(result)
}
export const main = (): void => {
const rl = readline.createInterface({ input, output })
console.log("Please insert the data.")
// reads line by line and calls someFunction(rl)(line)
rl.on('line', someFunction(rl))
}
You can check out the readline docs
Preparing the project for testing
The question is: how would we test our main
function that only calls our someFunction
?
If we mock readline
, we wouldn't be testing our app, we need to mock the stdin to get a realistic simulation of what our program is doing.
For that, we will use jest. In this particular project, these are my dependencies inside package.json
:
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@types/jest": "^27.0.3",
"@types/node": "^16.3.1",
"babel-jest": "^27.4.2",
"jest": "^27.4.3",
"mock-stdin": "^1.0.0",
"typescript": "^4.3.5",
// ...
},
The other config files:
// jest.config.ts
export default {
clearMocks: true,
testMatch: ['**/test/**/*.spec.ts'],
}
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2019",
"lib": ["ES2019"],
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
"esModuleInterop": true,
"strict": true
},
"include": ["./src/**/*", "./test/**/*"],
"exclude": ["node_modules"]
}
Testing
The package that does the trick is mock-stdin and is very simple to use:
// 1. import the lib
import mockStdin from 'mock-stdin'
// 2. start it
const stdin = mockStdin.stdin()
// 3. use it
stdin.send("some input")
// 4. end it
stdin.end()
Here is an example of a test for our main
function:
import mockStdin from 'mock-stdin'
import { main } from '../../src/main'
// mock console.log
console.log = jest.fn()
describe('main.ts', () => {
let stdin: ReturnType<typeof mockStdin.stdin>
// just a helper function to start the application
// and mock the input
const execute = (input: string): void => {
main()
stdin.send(input)
stdin.end()
}
beforeEach(() => {
stdin = mockStdin.stdin()
})
describe('when input is valid', () => {
const input = // something
const expectedResult = // another thing
beforeEach(() => {
execute(input)
})
it('should print the correct output', () => {
expect(console.log).toBeCalledWith(expectedResult)
})
})
// another describe blocks
}
That's it, folks, I hope this text helps you somehow!
Bye bye
Use masks (yes!) and use emacs
xoxo
covidVaccines.forEach(takeYourShot)
Cover photo: Photo by Sigmund
Posted on May 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.