概要
Paizaのスキルチェックテストのチュートリアルを、JavaScript(Node.js)とテスト駆動開発(TDD)でやってみたよ、ってメモ書き。
スキルチェックテストの実際の問題を解く上でも、「これで動くかな?」を確認するのは「テストで」やりたいよね、と思ったのでやってみた。
なお、ESLINT+VSCode環境をお勧めする。文法エラーで詰まるとかアホらしいので。 by 詰まった人
※スキルチェックテストの設問自体を公開するのはルール違反だが、チュートリアルなら良かろう。ほぼ「標準入出力の扱い方」でしかないし。
取り上げる設問
各問題とも「提出いただいたコードの実行(標準入力による値の取得、計算処理)→標準出力→正誤の判定」
とのことである。
チュートリアルでJavaScript言語を選択すると、次のようにデフォルトコードが出力される。
これは、標準入出力の扱い方のサンプルコード。
https://paiza.jp/challenges/practice
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行の文字列を標準出力へ出力する」と言う「機能を実装する」と捉えて、話を進める。
今後を見据えて、これをテスト駆動開発してみる。
機能をテストする枠組み
まず、次のような基本クラスを作った。
// 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 );
}
これを、サンプル―コードに倣って入出力から呼び出す。
/**
* [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
を継承して、例えば次のように行う。なお、現時点では敢えて出力は「期待値に対して未実装」にしてある。
// 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"
を次のように編集する。
"scripts": {
"start" : "node index.js",
"test": "cross-env NODE_ENV=development mocha --recursive"
},
今回の「標準入出力から入力された複数行の文字列のうち、終了時(Ctrl+C押下時)に最初の1行の文字列を標準出力へ出力する」機能を検証(テスト)するテストコードは、次のように書く。
/**
* [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
【補足】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()は不要。
機能を実装してテストする。
改めて実装する。
// 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
実際の動作も確認してみる。
実際のコマンドプロンプトからの入力をしてみる。
npm start
hoge
fuga
ctrl + c
qiita_tdd_on_tutorial_of_paiza_scrshot3_index.png
実際に、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()
するのが適切なのかな?w
補足
Googleで「paiza javascript テスト駆動」を検索したけど、javvascriptに関して、これってのはヒットせず。
Ruby向けだと見つかった。
エッ、今さら!?練習問題と具体的コード例によるTDD超入門。
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
書いたテストコードは以下。実装は省略する。
/**
* [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
判定無しで、とりあえず文字列出力まで実装。意図通りの失敗。
qiita_tdd_on_practice_scrshot2.png
実装終わって、無事にテストも成功。
ついでなので、実際のコンソール入力からの(システム)テストもやってみた。成功。
qiita_tdd_on_practice_scrshot3.png
今度こそ、以上ー。