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