DEV Community

Cover image for Writing and Testing a stdin script with TypeScript
Laura Viglioni
Laura Viglioni

Posted on

Writing and Testing a stdin script with TypeScript

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
Enter fullscreen mode Exit fullscreen mode

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))
}

Enter fullscreen mode Exit fullscreen mode

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",
    // ...
  },

Enter fullscreen mode Exit fullscreen mode

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"]
}

Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

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

 }

Enter fullscreen mode Exit fullscreen mode

That's it, folks, I hope this text helps you somehow!
Bye bye

Use masks (yes!) and use emacs
xoxo

covidVaccines.forEach(takeYourShot)
Enter fullscreen mode Exit fullscreen mode

Cover photo: Photo by Sigmund

Top comments (2)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Love reading TypeScript articles 😬

Collapse
 
righttrianglesrkewl3 profile image
Kevin Zehnder

Really helpful thank you