readlineを使ったNode.jsで簡易な標準入出力とモックのテスト

2 min read読了の目安(約2500字

環境

  • node.js 15.14.0
  • npm 7.11.1
  • typescript 4.2.4
  • ts-node 9.1.1
  • jest 26.6.3
  • ts-jest 26.5.5

はじめに

node.jsで標準入力で処理対象を受け取って、加工した後に標準出力するプログラムを書くことがありました。
その際のテストを書くのに少しハマったので、ここで共有します。

簡単な標準入出力を扱うNode.jsプログラム

例えば以下のようなものを考えてみます。

空白で区切られた文字列を改行で区切られ、複数列が入力される。
文字列の空白の前をkey、後をvalueとしたコレクションを作成する。
ただ、既に出現しているkeyの場合は、そのvalueは無視する。(最初のvalueを結果とする)
最後にそのkey-valueのコレクションをjson文字列で標準出力に出す。

test.txt
hello world
foo baz
hoge fuga
hoge piyo
npx --silent ts-node index.ts < test.txt > result.json
result.json
{"hello":"world","foo":"baz","hoge":"fuga"}

標準入出力を使うと、このように簡単に入力を受け取ったり、出力をパイプを使ってjq等の別のプログラムに渡すなどが容易なのがいいところですね。

Node.jsのreadlineモジュールを使って、簡単なプログラムを書くとこんな感じでしょうか。[1]

index.ts
import * as readline from "readline";

const r = readline.createInterface({
  input: process.stdin,
  terminal: false,
});

const result: { [key in string]: string } = {};

r.on("line", (line) => {
  const elements = line.split(" ");
  result[elements[0]] = result[elements[0]] ?? elements[1];
});

r.on("close", () => {
  console.log(JSON.stringify(result));
});

テスト

プログラムを書いたら、テストを書くというのが世の常人の常というもの。
今回はこのように書きました。
環境で述べたとおり、テストフレームワークはjestです。

index.test.ts
import { sendLine, sendClose } from "./mock-stdin";

test("input lines has duplicate key", () => {
  // given
  const spyConsoleLog = jest.spyOn(console, "log");

  // when
  require("./index");
  sendLine("hello world");
  sendLine("foo baz");
  sendLine("hoge fuga");
  sendLine("hoge piyo");
  sendClose();

  // then
  expect(spyConsoleLog).toBeCalledWith(
    '{"hello":"world","foo":"baz","hoge":"fuga"}'
  );
});
mock-stdin.ts
let sendLine: (line: string) => void;
let sendClose: () => void;

jest.mock("readline", () => {
  return {
    createInterface: () => {
      return {
        on: (event: string, callback: (...args: any) => void) => {
          switch (event) {
            case "line":
              sendLine = callback;
              break;
            case "close":
              sendClose = callback;
              break;
          }
        },
      };
    },
  };
});

export { sendLine, sendClose };

標準入出力の部分とロジックの部分を分けて、ロジックのみをテストするのも全然OKです。
むしろ通常はそちらの方が良いと思いますが、今回はこのようにしています。

最後に

jestのmockは強力なので、こんなこともできちゃいます。
ただ、使いすぎると結局何をテストしているんだ、となることもあるのでご注意ください。[2]

リポジトリ

今回のサンプルプログラムのリポジトリはこちらです。

脚注
  1. 予期しない入力で簡単にエラーが起きるコードですが、そこは目をつぶってほしいです ↩︎

  2. 自戒です ↩︎