LoginSignup
4
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-26

概要

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

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

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

取り上げる設問

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

とのことである。

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

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行の文字列を標準出力へ出力する」と言う「機能を実装する」と捉えて、話を進める。

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

機能をテストする枠組み

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

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

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

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を継承して、例えば次のように行う。なお、現時点では敢えて出力は「期待値に対して未実装」にしてある。

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" を次のように編集する。

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

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

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

【補足】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()は不要。

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

改めて実装する。

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

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

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

npm start
hoge
fuga
ctrl + c

qiita_tdd_on_tutorial_of_paiza_scrshot3_index.png
https://gyazo.com/5ff81418690a1ad89c3cc667bc8198cd

実際に、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の煩わしいテスト(入力処理)を自動化したい

最初は、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
/**
 * [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

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

qiita_tdd_on_practice_scrshot2.png
https://gyazo.com/dceda4eccc304408ad0bc790b4012d2f

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

qiita_tdd_on_practice_scrshot3.png
https://gyazo.com/d59dca0df97f140dbe22f38b42a78f26

今度こそ、以上ー。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2