Paizaのチュートリアル問題をTDDしてみるメモ

# 概要

Paizaのスキルチェックテストの**チュートリアル**を、JavaScript(Node.js)とテスト駆動開発（TDD）で**やってみた**よ、ってメモ書き。

スキルチェックテストの実際の問題を解く上でも、「これで動くかな？」を確認するのは「テストで」やりたいよね、と思ったのでやってみた。
なお、ESLINT＋VSCode環境をお勧めする。文法エラーで詰まるとかアホらしいので。 by 詰まった人

※スキルチェックテストの設問自体を公開するのはルール違反だが、チュートリアルなら良かろう。ほぼ「標準入出力の扱い方」でしかないし。



# 取り上げる設問

> 各問題とも「提出いただいたコードの実行(標準入力による値の取得、計算処理)→標準出力→正誤の判定」

とのことである。

チュートリアルでJavaScript言語を選択すると、次のようにデフォルトコードが出力される。
これは、標準入出力の扱い方のサンプルコード。
https://paiza.jp/challenges/practice

```js:tutorial.js
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// スキルチェックの基本となる、標準入力で値を取得し、
// 出力するコードを書いてみよう！

var lines = [];
var reader = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
});
reader.on('line', (line) => {
  lines.push(line);
});
reader.on('close', () => {
  console.log(lines[0]);
});
```

これを「標準入出力から入力された複数行の文字列のうち、終了時（Ctrl+C押下時）に最初の1行の文字列を標準出力へ出力する」と言う「機能を実装する」と捉えて、話を進める。

今後を見据えて、これをテスト駆動開発してみる。


# 機能をテストする枠組み

まず、次のような基本クラスを作った。

```js:paiza_stdio.js
// var PAIZA_STANDARD_IO = require("paiza_stdio.js").PAIZA_STANDARD_IO;
/**
 * Readlineオブジェクトのイベント"line"と"close"の発火時に
 * これを呼び出すように実装する。
 * 標準入出力に対する機能実装は、このクラスを継承して行う。
 * 
 * @param {Object} Consoleオブジェクトのインスタンスを指定。通常は"console"を書けばよい。
 */
var PAIZA_STANDARD_IO = function ( consoleInstance ) {
  this._lines = [];
  this._consoleInstancce = consoleInstance;
};
PAIZA_STANDARD_IO.prototype.onInputLine = function (lineStr) {
  // ↑呼び出し元のインスタンスをthisに欲しいので、アロー関数は使わない。
  this._lines.push( lineStr );
};
PAIZA_STANDARD_IO.prototype.onClose = function () {
};
PAIZA_STANDARD_IO.prototype.writeConsole = function (value) {
    var str = value.toString();
    this._consoleInstancce.log( str );
}

```

これを、サンプル―コードに倣って入出力から呼び出す。


```js:index.js
/**
 * [index.js]
 */
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// スキルチェックの基本となる、標準入力で値を取得し、
// 出力するコードを書いてみよう！


// IO for target environment.
var reader = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
});
reader.on('line', (line) => {
  instance.onInputLine( line );
});
reader.on('close', () => {
  instance.onClose();
});


// Paizaへの提出時は、忘れずに【以下を】ロード先のコードに置き換えること！
var TUTORIAL_SHOW_1ST_LINE = require("./src/01_tutorial.js").TUTORIAL_SHOW_1ST_LINE;
var instance = new TUTORIAL_SHOW_1ST_LINE( console );
```


機能の実装、今回のサンプルコードであれば「標準入力の最初の1行目を、終了後に標準出力へ表示」は、`PAIZA_STANDARD_IO`を継承して、例えば次のように行う。なお、現時点では敢えて出力は「期待値に対して未実装」にしてある。


```js:01_tutorial.js
// Implement
var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) {
  PAIZA_STANDARD_IO.call(this, consoleInstance);
};
TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype );
TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () {
  this.writeConsole( "終了時に出力する文字列" );
};
exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE;


```

# 作りたい機能を検証（テスト）するコードを作成

続いてテストコード。
Mocha＋Chai＋Sinon環境が好きなので、次コマンドを叩いてサクッとインストールする。cross-envは「テスト用の環境変数の取り扱いをクロスOSで容易にする」モジュールで、今は使わないかもしれないが後々を考えて入れておく。`npm init`は特にこだわりなければ全部デフォルトで「Yes」でOK。

```
npm init
npm install --save-dev mocha chai cross-env sinon
```

package.jsonの `"script"` を次のように編集する。

```json:package.json
  "scripts": {
    "start" : "node index.js",
    "test": "cross-env NODE_ENV=development mocha --recursive"
  },

```


今回の「標準入出力から入力された複数行の文字列のうち、終了時（Ctrl+C押下時）に最初の1行の文字列を標準出力へ出力する」機能を検証（テスト）するテストコードは、次のように書く。


```js:01_tutorial_test.js
/**
 * [01_tutorial_test.js]
 * encoding=utf-8
 */

var chai = require("chai");
var expect = chai.expect;
var sinon = require("sinon");


describe( "01_tutorial.js", function(){
    var target = require("../src/01_tutorial.js");

    describe("instance.",function () {
        var instance = new target.TUTORIAL_SHOW_1ST_LINE( console );


        it("After input 'hoge', 'fuga' & ctrl+c`, output `hoge`.", function () {
            var consoleStubLog = sinon.stub( console, "log" );
            var INPUT1 = "hoge", INPUT2 = "fuga";

            instance.onInputLine(INPUT1);
            instance.onInputLine(INPUT2);
            instance.onClose();

            consoleStubLog.restore();
            expect( consoleStubLog.getCall(0).args[0] ).to.equal( INPUT1 );
        });
    });
});

```

これを実行すると次のようになる。現時点では、「失敗」が期待値。ここから開始。

```
npm test
```

qiita_tdd_on_tutorial_of_paiza_scrshot1_failed.png
![https://gyazo.com/dd93e4c42955d519d4dc1bfc0249daa6](https://gyazo.com/dd93e4c42955d519d4dc1bfc0249daa6.jpeg
)

## 【補足】consloe自体をテスト用のスタブに置き換える

上記のサンプルコードでは、ビルトインのconsoleインスタンスに対して「log()をスタブ化する」操作をしている。これはこれで「何がどう置き換わったか？」の観点で分かり居やすい。だけど、動作エラーの出力先もスタブ化されたlog()に置き換わってしまい出力されない。テストを繰り返しながらデバッグする観点だと、使いづらいことに後で気づいた。

```
var instance = new target.TUTORIAL_SHOW_1ST_LINE( console );
var consoleStubLog = sinon.stub( console, "log" );
// (中略)
consoleStubLog.restore();
```

debugの観点だと、ストラテジーパターンの考え方で「テストしたい関数で使うconsoleインスタンスだけを、テスト用に差し替える」の方が望ましい。この場合、動作エラー時の出力や、`console.log()`によるprintfデバッグも利用可能となる。

```
var consoleStubLog = sinon.stub();
var consoleStub = { "log" : consoleStubLog };
var instance = new target.TUTORIAL_SHOW_1ST_LINE( console );
// restore()は不要。
```


# 機能を実装してテストする。

改めて実装する。

```js:01_tutorial.js
// Implement
var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) {
  PAIZA_STANDARD_IO.call(this, consoleInstance);
};
TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype );
TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () {
  this.writeConsole( this._lines[0] );
};
exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE;

```

テストを実行すると、成功する。

```
npm test
```

qiita_tdd_on_tutorial_of_paiza_scrshot2_ok.png
![https://gyazo.com/8e14f457c2545a5af96d4b594c9abf40](https://gyazo.com/8e14f457c2545a5af96d4b594c9abf40.jpeg)


# 実際の動作も確認してみる。

実際のコマンドプロンプトからの入力をしてみる。

```
npm start
hoge
fuga
ctrl + c
```


qiita_tdd_on_tutorial_of_paiza_scrshot3_index.png
![https://gyazo.com/5ff81418690a1ad89c3cc667bc8198cd](https://gyazo.com/5ff81418690a1ad89c3cc667bc8198cd.jpeg
)


実際に、Paizaに以下のコードを提出してみる。
「成功」になる。

```
/**
 * [index.js]
 */
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// スキルチェックの基本となる、標準入力で値を取得し、
// 出力するコードを書いてみよう！




// IO for target environment.
var reader = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
});
reader.on('line', (line) => {
  instance.onInputLine( line );
});
reader.on('close', () => {
  instance.onClose();
});


// Paizaへの提出時は、忘れずに【以下を】ロード先のコードに置き換えること！
// var TUTORIAL_SHOW_1ST_LINE = require("./src/01_tutorial.js").TUTORIAL_SHOW_1ST_LINE;
/**
 * [01_tutorial.js]
 */
// var PAIZA_STANDARD_IO = require("paiza_stdio.js").PAIZA_STANDARD_IO;
var PAIZA_STANDARD_IO = function ( consoleInstance ) {
  this._lines = [];
  this._consoleInstancce = consoleInstance;
};
PAIZA_STANDARD_IO.prototype.onInputLine = function (lineStr) {
  this._lines.push( lineStr );
};
PAIZA_STANDARD_IO.prototype.onClose = function () {
};
PAIZA_STANDARD_IO.prototype.writeConsole = function (str) {
  this._consoleInstancce.log( str )
}


// Implement
var TUTORIAL_SHOW_1ST_LINE = function ( consoleInstance ) {
  PAIZA_STANDARD_IO.call(this, consoleInstance);
};
TUTORIAL_SHOW_1ST_LINE.prototype = Object.create( PAIZA_STANDARD_IO.prototype );
TUTORIAL_SHOW_1ST_LINE.prototype.onClose = function () {
  this.writeConsole( this._lines[0] );
};
exports.TUTORIAL_SHOW_1ST_LINE = TUTORIAL_SHOW_1ST_LINE;


var instance = new TUTORIAL_SHOW_1ST_LINE( console );
exports.instance = instance;

```

以上ー。


# 雑感

今回に、次のコードを書いたときに、「あぁ、そうか！Sinon::stub()って、この用途がスタート地点なんだ！」て妙に腑に落ちたの印象的。実際にどうかは知らんが、私にはしっくりきた。

```
var instance = new target.TUTORIAL_SHOW_1ST_LINE( console );
// (中略)
var consoleStubLog = sinon.stub( console, "log" );
// (中略)
consoleStubLog.restore();
```
これ、`sinon.stub()`をしないと当然ながら、そのままコンソールに `console.log()` した値が出力される。でも、上記のように`stub()`ってから実行すると、出力されない。そして、`restore()` した以降は、元のように出力されるようになる。

あぁ、そういうことなのね。シノンのスタブ、完全に理解した（）。

※なお、`sinon.stub()`してると、デバッグ目的でのconsole.log()は使えなくなります、当然ながら。その場合は `sinon.spy()`にすれば解決するかな？　いや、そもそもconsole.log()じゃなくて、`.writeConsole()`を `sinon.stub()`するのが適切なのかな？ｗ


# 補足

Googleで「paiza javascript テスト駆動」を検索したけど、javvascriptに関して、これってのはヒットせず。

Ruby向けだと見つかった。

[エッ、今さら！？練習問題と具体的コード例によるTDD超入門。](https://paiza.hatenablog.com/entry/2014/09/04/%E3%82%A8%E3%83%83%E3%80%81%E4%BB%8A%E3%81%95%E3%82%89%EF%BC%81%EF%BC%9F%E7%B7%B4%E7%BF%92%E5%95%8F%E9%A1%8C%E3%81%A8%E5%85%B7%E4%BD%93%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B%E3%81%AB%E3%82%88)

C#版だと、こんなのもあった。
[【C#】paizaの煩わしいテスト（入力処理）を自動化したい]
(https://kuroeveryday.blogspot.com/2014/11/paiza-TDD.html)



最初は、ReadLineをフックすることを考えた。
https://nodejs.org/api/readline.html#readline_event_line

次に、Readable Streamsをフックするも検討してみた。
https://nodejs.org/api/stream.html#stream_readable_streams

でも、どっちもピンとこなかった。


そこまで考えて、「呼び出しのところだけ切り離せばよいのでは？標準入出力のところを律義にエミュレートする必要は無い」と考え直した。そして書いたコードが上記である。


これでいいのかは分からないが、とりあえずしっくりは来た。
「それは違う」とか「こうした方が良い」とかのコメント歓迎。


# 補足の補足（サンプルで実施例）

コーディングサンプル問題として「入力された整数がグレゴリオ暦でうるう年であるか判定する」があったので、やってみた。
https://paiza.jp/challenges/practice

書いたテストコードは以下。実装は省略する。

```js:02_tutorial_practice_test.js
/**
 * [02_tutorial_practice_test.js]
 * encoding=utf-8
 */

var chai = require("chai");
var assert = chai.assert;
var expect = chai.expect;
var sinon = require("sinon");

describe( "02_tutorial_practice.js", function(){
    var target = require("../src/02_tutorial_practice.js");

    describe("instance.",function () {
        var instance = new target.IS_LEAP_YEAR( console );

        it("入力された整数がグレゴリオ暦でうるう年であるか判定する", function () {
            var consoleStubLog = sinon.stub( console, "log" );

            // 【コーディングサンプル問題】
            // 1行目には、入力される行数Tが入ります。
            // 1回のテストケースは、1行に1つずつ整数Nが入っている複数行の標準入力（stdin）による入力になります。 
            // https://paiza.jp/challenges/practice
            instance.onInputLine("4");
            instance.onInputLine("1000");
            instance.onInputLine("1992");
            instance.onInputLine("2000");
            instance.onInputLine("2001");
            instance.onClose();
 
            consoleStubLog.restore();
            expect( consoleStubLog.callCount ).to.equal( 4 );
            expect( consoleStubLog.getCall(0).args[0] ).to.equal( "1000 is not a leap year" )            
            expect( consoleStubLog.getCall(1).args[0] ).to.equal( "1992 is a leap year" )            
            expect( consoleStubLog.getCall(2).args[0] ).to.equal( "2000 is a leap year" )            
            expect( consoleStubLog.getCall(3).args[0] ).to.equal( "2001 is not a leap year" )            
        });
    });
});

```

実装しながらのユニットテストの「失敗と成功」は次のように進んだ（実装例は省略）。

とりあえず枠だけ実装してテスト。もちろん失敗。

qiita_tdd_on_practice_scrshot1.png
![https://gyazo.com/efad38f2c9fdd263a6ee4f4adc4afbb8](https://gyazo.com/efad38f2c9fdd263a6ee4f4adc4afbb8.jpeg)

判定無しで、とりあえず文字列出力まで実装。意図通りの失敗。

qiita_tdd_on_practice_scrshot2.png
![https://gyazo.com/dceda4eccc304408ad0bc790b4012d2f](https://gyazo.com/dceda4eccc304408ad0bc790b4012d2f.jpeg)

実装終わって、無事にテストも成功。
ついでなので、実際のコンソール入力からの（システム）テストもやってみた。成功。

qiita_tdd_on_practice_scrshot3.png
![https://gyazo.com/d59dca0df97f140dbe22f38b42a78f26](https://gyazo.com/d59dca0df97f140dbe22f38b42a78f26.jpeg)


今度こそ、以上ー。