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
Top comments (2)
Love reading TypeScript articles 😬
Really helpful thank you