びぼーろくっ!

誰かに見せるわけでもないけど、備忘録として。。

React.jsのコンポーネントのテスト!

今回テストをするにあたってenzymeを導入しました。
(エンザイムってカタカナで書くと健康食品みたい。。)

github.com

実際の実装の仕方は下記の記事が参考になりました。
labotech.dmm.com
(DMMさん!こんなところでもお世話になるなんて!)

今回の記述したコードはこちらです。

//react.component.test.js
import React from 'react';
import testCommon from './testCommon';
import mocha from 'mocha';
import sinon from 'sinon';
import assert from 'power-assert';
import mochaSinon from 'mocha-sinon';
import { shallow, mount } from 'enzyme';
import EmailInput from '../testTarget/EmailInput';

describe('EmailInputのテスト', () => {
  it('propが渡されたときにvalueにセットされること', () => {
    const wrapper = shallow(<EmailInput />);
    // ここでpropsをセットする
    wrapper.setProps({ 'value': 'kinachan0725@example.com' });

    const input = wrapper.find('input');
    assert.deepEqual(input.node.props.value,'kinachan0725@example.com');
  });
  
  it('メールアドレスが入力されたときにonChangeイベントが発火すること', () => {
    // onChangeメソッドが呼ばれたときの入出力(引数の値や戻り値、呼ばれた回数など)を監視する
    const onChange = sinon.spy();
    // テスト対象のコンポーネントのみをレンダリング
    const wrapper = shallow(<EmailInput onChange={onChange} />);

    // 入力イベントを擬似的に再現
    wrapper.find('input').simulate(
      'change', {
        target: {value: 'x'}
      }
    );
    // onChangeメソッドが1回呼ばれているかを確認
    assert(onChange.calledOnce === true);
  });
});
//テスト対象ファイル EmailInput.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class EmailInput extends Component {
  render() {
    return (
      <div>
        <input
          type="text"
          name="email"
          ref={(email) => { this.textInput = email; }}
          placeholder="メールアドレス"
          value={this.props.value}
          onChange={this.props.onChange}
        />
      </div>
    );
  }
}

//propTypesで型と初期値を宣言
EmailInput.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func,
};

EmailInput.defaultProps = {
  value: undefined,
  onChange: undefined,
};
export default EmailInput;

とまあ、ほぼコピペです。
ただ、実際に変更したのはこちら。

今のプロジェクトではeslintを導入して、ルールにprop-typesが指定されていない場合エラーになるので
ESLint 最初の一歩 - Qiita
ESLintの導入と警告対応のメモ - Qiita
(公式よりこちらのが分かり易いのでQittaの記事を)

EmailInputにthis.props.valueと指定したら怒られます。
(eslintのルールを変更すればいいんですけど、それは入れた意味がないw)

これはテスト対象の2行目にimportしている
PropTypesで解決します。
要はpropsの中に好き勝手にプロパティを使うのではなく、ちゃんと宣言しろってことですね。

また、テストコードですが、参考URLとは異なりassertにpower-assertを使用しているので組み替えました。
propsの値はinput.node.props.valueで取れるみたいですね。

ちなみにテスト実行時コンソール上で

Warning: Shallow renderer has been moved to react-test-renderer/shallow. Update references to remove this warning.

が表示されましたが、これで解決できそうです。
Support react-dom/test-utils · Issue #875 · airbnb/enzyme · GitHub

とりあえずテストは通るので、来週辺りにこれ解決しようかな。。。

Mocha + Sinon + Nockを使ったテスト

原因が分かるまで大量に時間を要したので忘れないようにメモっておこう。。

Nockはこの記事を参考にしました。
MochaとNockでモックサーバーを作ってレスポンスのテスト | MMMブログ

ポイントはnockのgetを空文字にしたところ
問題なく通信が行われたこと。
これを指定せずにallusersなどに指定したら
実際に通信が行われてnock化されていない事が判明。。
詳しい原因は調べてみないと分からないけど、とにかく動いた。。

//testファイル
describe('Nockを使ったテスト', () => {
  const exsample = 'http://exsample.com/';
  let stub;
  let target;
  beforeEach('setup', (done) => {
    target = new Target();
    done();
  });
  afterEach('teardown', (done) => {
    stub.restore();
    target = null;
    done();
  });
  //Nockを使ったテスト
  it('use nock', (done) => {
    //メソッドをスタブ化
    stub = sinon.stub(target, 'getBaseUrlByRequest');
    stub.returns(exsample);
    
    //Nockを作成
    var nockRequest= nock(`${exsample}allusers`, {
      reqheaders: {
        'content-type': 'application/json'
      }
    }).get('').reply(200, {
      _id: 'kinachan0725',
      _rev: '946B7D1C',
      username: 'きなちゃん',
      email: 'kinachan0725@exsample.com'
    });
    let result = target.readAllUsers('allusers', null, 'req')
      .then((data) => {
        console.log('data', data);
        assert(data !== undefined);
        done();
      }, (err) => {
        console.log('err', err);
        done();
      });
  });
});
// テスト対象のファイル
export default class Target {
  readAllUsers= async(url, base = null, req = null) => {
    let baseUrl;
    if (base != null) {
      baseUrl = base;
    } else if (req != null) {
      //スタブ化されているので、無条件で全てexsample.comが返却される
      baseUrl = this.getBaseUrlByRequest(req);
    } else {
      baseUrl = typeof window === 'undefined' ? this.BaseUrl : window.location.origin;
    }
    //nockで設定したものが返却される
    const res = await fetch(`${baseUrl}${url}`, {
      method: 'GET',
      credentials: 'same-origin',
      headers: {
        'content-type': 'application/json'
      },
    });
    const json = await res.json();
    return json;
  }
}
//コンソール上の出力結果
data { _id: 'kinachan0725',
  _rev: '946B7D1C',
  username: 'きなちゃん',
  email: 'kinachan0725@exsample.com' }

mochaの例外テストを組みました。

mocha + power-assertのExceptionテストを実装しました。
前回の記事などでmochaやpower-assertは説明しているので省略します。

//test.js


describe('1件成功+1件例外のテスト', () => {
  before('setup',(done) => {
    target= new Target(); //Test対象メソッドのコンストラクタ作成
    done();
  });
  it('success',(done) => {
    const val = 10;
    //valが数値の為、正常に終了
    const result = target.ValueTwoTimes(val);
    assert(val*2 === result);
    done();
  });

  // 共通クラスの中にAssertExceptionを作成しました。
  // 第一引数にテストメソッド(bindで紐付ける)、
  //第二引数に期待値(エラーオブジェクト)を渡してテストを実行します。
  it('exception', (done) => {
    const val = 'あ';
    let error = new Error('valueは数値でなければなりません!');
    testCommon.AssertException(async.ValueTwoTimes.bind(this,val),error);
    done();
  });
});
import assert from 'power-assert';

export class TestCommon {
 //例外テストを実行し、検証をします。
  AssertException = (callBackFunc,expectedError) => {
    const result = this.exceptionMethodRun(callBackFunc);
    if (result === undefined) assert(result === undefined,'例外処理未発生の為エラー');

    assert(expectedError.message === result.message,'例外処理メッセージ不一致');
  }

 //例外テストを実行しExceptionを返します。
  exceptionMethodRun = (callBackFunc) => {
    let error = undefined;
    try {
      callBackFunc();
    } catch (exception) {
      error = exception;
    }
    return error;
  }
}

const testCommon = new TestCommon();
module.exports = testCommon;
//テスト対象メソッド
export default class Target {
  ValueTwoTimes = (value) => {
    if (typeof value !== 'number') {
      throw new Error('valueは数値でなければなりません!');
    }
    const number = parseInt(value, 10);
    return number * 2;
  }
}
module.exports = Target;

原理は簡単でcallBackで渡されたテスト実行メソッドをtry~catchの中で実行しているだけです。
Exceptionのテストでいちいち記述量が増えてしまうと大変なので作りました。

テストカバレッジを入れました。

テストカバレッジを入れました。

テストカバレッジのnycをmochaに入れました。
mochaでテストの網羅率を調べたいという際に便利です。
使い方やインストール方法はこちら↓

Using Istanbul With Mocha

//テスト対象メソッド
const red = 1;
const blue = 2;
const green = 3;
const black = 4;

exports.isNull = (val) => {
  return val === null;
}

exports.colorString = (color) => {
  const result = {};
  if (color === red) {
    result.color = 'red';
  }

  if (color === blue) {
    result.color = 'blue';
  }

  if (color === green) {
     result.color = 'green';
  }

  if (color === black) {
     result.color = 'black';
  }
  return result;
};
//テストコード
import mocha from 'mocha';
import assert from 'power-assert';

function requireColor(colorCode) {
  const color = require('../testTarget/color');
  const colorStr = color.colorString(colorCode);
  return colorStr;
}


describe('color.js', () => {
  it('red', () => { // テストケース
    const colorString = requireColor(1); // test対象メソッド
    assert('red' === colorString.color); // 検証
  });
  it('blue', () => {
    const colorString = requireColor(2);
    assert('blue' === colorString.color);
  });
  it('green', () => {
    const colorString = requireColor(3);
    assert('green' === colorString.color);
  });
});

こんなソースがあったとしましょう。
その場合、nycを実行した場合このような結果が表示されました。

f:id:kinachan0725:20170726172920p:plain
(必要のないテストはマスクかけました。)

各画面の見方です。

Stmts : プログラムの各ステートメントは実行されましたか?(調べたら命令網羅と同義でした)
Branch : ifやcaseなどの全ての分岐の処理が実行されたか否か?
Funcs:各関数が呼び出されたかの網羅率
Line : ソースファイルの各実行可能行が実行されたか
Uncovered Line: Lineの対象を示す行番号

stackoverflowの文言を自分なりに翻訳しました。
間違っていたら教えてください・・・

testing - How do I read an Istanbul Coverage Report? - Stack Overflow

まずは分かり易いところから
Funcsが50%になっているのはisNull未実行&colorString実行の為です。
各関数が全てテスト実行されれば100%になります。

Uncovered Line・・全テストの中で処理が走っていない部分を記述されます。
下記2行が走っていない為ですね。

  return val === null; //8行目
  result.color = 'black'; //23行目

Branch・・各ifやcaseなどの分岐の処理が走っているか

    if (color === back)

この処理が走っていない為でしょう。

Stmtsは置いといてisNullの関数を削除した場合、値はどう変化するでしょうか。
試してみます。

f:id:kinachan0725:20170726173405p:plain

Funcsが100%になり、Lineも88.24⇒93.33、Stmtsも88.24⇒93.33に変化しましたね。
isNullの処理を削除したのでresult.color = 'black'; が23行目から10行目に変化しています。

ただ・・Stmtsの値の変化に関しては理解出来ませんでした。
こんな感じのテストとメソッドを書いてみたのですが・・・

//先ほどのテスト対象のコードに追記しています。
exports.ifCheck = (isHappy, isRich) => {
  let youState = 'You are ';
  if (isHappy) {
    youState += 'Happy';
  }
  if (isRich) {
    if (isHappy) youState += '&';
    youState += 'Rich';
  }
  if (!isHappy && !isRich) {
    youState = 'oh my god...';
  }
  return youState;
};
//同じくテストコードに追記
describe('testCase2', () => {
  it('You are Happy&Rich', () => {
    const result = requireifCheck(true, true);
    assert('You are Happy&Rich' === result);
  });
  it('You are Happy', () => {
    const result = requireifCheck(true, false);
    assert('You are Happy' === result);
  });
  it('oh my god...', () => {
    const result = requireifCheck(false, false);
    assert('oh my god...' === result);
  });
});

f:id:kinachan0725:20170726173614p:plain

んんん~~・・・
なぜStmtsが100になるのだ・・・
(今回の場合だとfalse,trueのパターンが存在しないためBranchが94%になっている)
それぞれ渡された引数がtrue,falseで存在するからかな。
まだまだ検証が必要のようです。

追記:パラメーターは関係なくそのままの命令網羅のようです。

今日は直交表に関して勉強しました。

テストロジックに関して知識がなかったので(他の知識も皆無だがw)
今回テスト計画を立てる際に指標になればと思い勉強しました。
数学はてんで駄目なので、分かり易いサイトを探すのに苦労しました。

テストパターンを効率化!直交表でテスト工数を最低限に! | geechs magazine

このサイトを参考に自分で直交表を作ってみます。
元バンドマンっぽくチケット管理システムを考えてみましょう。

f:id:kinachan0725:20170725165824p:plain

ひどい対バンの組み合わせですね。
誰がこんなイベント行くのでしょうか・・・

何のためには置いといて、これらを入力するWEBアンケートがあったとします。
このWEBアンケートのテストを網羅的にやるとなると。。

男性-当日-j-pop
男性-当日ーメタル
...etc

f:id:kinachan0725:20170725170027p:plain

2×3×4=24通りのテストが必要になります。
こんなのやってられません。帰りたいです。

そのため、頭のいいお偉いさんが直交表ということを考え出しました。
これは統計学に基づいて
「不具合が起こる要因のほとんどは2つまでの要因の組み合わせによるものだ」
という結論になったといわれます。これで90%以上はカバーできるとの事。

ならばこれらを使ってテスト工数を削減して、
効率のいいテストを組み立てていきたいと思います。

それでは先ほどのチケット管理システムの例を使って直交表を組み立てていきます。

f:id:kinachan0725:20170725170052p:plain

まずはこんな感じで並べていきます。
これで男性&支払い方法 or 目当ての組み合わせは網羅されました。

f:id:kinachan0725:20170725170128p:plain

続いて女性を入力していきます。
女性&支払い方法 or 目当ての組み合わせも網羅されました。
あとは支払い方法&目当ての組み合わせで足りない所を埋めていくだけですね。

f:id:kinachan0725:20170725170151p:plain

この表だと偏りがあるか分かり辛いのでソートして、Noを振ります。

f:id:kinachan0725:20170725170215p:plain

全12パターンのうち
男性と女性が6パターンずつ
支払い方法が4パターンずつ
目当てが3パターンずつ
綺麗に網羅されていることが分かりますね。

当然、これらの目当てに
性別:中性とか目当てにブルース・R&Dなども増えていくと総当りだとパターン数が増えていきます。
(中性という性別は存在しませんが。。。)
ただ、これらの直交表を使えば複雑なテストほど工数が減っていくので効果的ですね。

mochaのonlyを検知する。

今回の記事もmochaです。
mochaのonlyメソッドは付与されているテストのみ実行する事が出来るので
特定のテストの動作確認をしたい。って時に活用できるかと思います。

ただ、コミット時onlyを削除し忘れて
本来なら失敗するテストも成功してしまい気付かないケースがあります。
そうなるとテストがSkipされてテストで気付けない⇒よって手動実行みたいなケースも想像できます。
となると本末転倒です。。

そこでtest実行時、テスト対象のファイルから.onlyが付与されているものを
エラーとして返してくれるpackageをインストールしました。

dot-only-hunter
GitHub - dasilvacontin/dot-only-hunter: Hunt down `.only`s before it's too late.

まずはインストール。本番環境では不要なのでdevのみにインストールします。

npm install --save-dev dot-only-hunter

その後、package.json内のscriptsの中に下記を追加します。

    "dot-only-hunter":"dot-only-hunter test/**/*.js",
    "test": "dot-only-hunter && NODE_ENV=test mocha --recursive --compilers js:babel-register test/*",

test内で&&で囲み先にdot-only-hunterが実行するようにします。
これでdot-only-hunterが先にtestディレクリ内の.onlyを検知してくれて
onlyが付与されている状態であればテストをしないで失敗します。
skipされるのにテストを全体で実行しても意味がないので。。

対象のソースコード

    describe.only('clacTest', () => {
      it('testCase1',() => {
        testRunAndVerifycation(100,108);
      });

      it('testCase2', () => {
        testRunAndVerifycation(200,216);
      });
    });

実行してみます。

clac.js
f:id:kinachan0725:20170724164117p:plain

きちんと.onlyを検知できているみたいです。
では、.onlyを外してみます。

f:id:kinachan0725:20170724164818p:plain
ちゃんとdot-only-hunterが成功になり、その後のテストコマンドを実行しているのが分かります。

また、今回dot-only-hunterを適用するのはnpm testコマンドのみで
前回の記事でお伝えしたtest-debugのコマンドには適用しません。
describeにonlyを付与した状態でtest-debugを叩きながらテストコードを作成し、
終了後にonlyを削除する。もし忘れてしまったらnpm testで検知してくれるという運用を考えています。

mochaのテスト実行時、VSCodeでES6のコードに埋め込んだブレークポイントを有効にしたい。

結構設定が大変でした。。
やり方を乗せておきます。

インストールされている環境

mocha
babel-register

babel-registerを有効にしないで、そのままmochaコマンドを実行すると
import文などのES6記述で構文エラーが発生します。
上記自体はpackage.jsonのscriptに
babel-registerでコンパイルしてあげればOKですが
例:

"test": "NODE_ENV=test mocha --recursive --compilers js:babel-register test/*",

これをVSCode上で適用したい。。と思ったのが背景でした。

設定は下記のようにしました。

//■launch.json
{
   "version" : "0.2.0",
   "configurations" : [
      {
        "name" : "Run mocha",
        "program" : "${workspaceRoot}/node_modules/mocha/bin/_mocha",
         "request" : "launch",
         "args" : [
            "--compilers",
            "js:babel-core/register",
            "--recursive",
            "--debug-brk",
            "test/**/*.js"
         ],
         "cwd" : "${workspaceRoot}",
         "env" : {
            "NODE_ENV" : "development"
         },
         "protocol" : "inspector",
         "runtimeExecutable" : null,
         "stopOnEntry" : false,
         "type" : "node"
      }
   ]
}

nameはいわずがなでprogramにmochaのファイルを読み込ませます。
argsにmochaに渡す引数オプションを渡しています。
"--compilers","js:babel-core/register"とそれぞれargsに個別に渡していいのね。。
って事をしらなくて小一時間悩みましたw

続いてpackeage.jsonのscriptsの中に下記を追加

"test-debug": "NODE_ENV=test mocha --recursive --debug-brk --compilers js:babel-register test/**/*.js"

今回はtest-debugと命名しましたが何でもいいと思います。(npmコマンドに存在しなければ)
ですが、testコマンドと違って--debug-brkを入れないとコマンドが終了してしまいブレークポイントにヒットしません。

↑これがなくても動きました。。launch.jsonに設定するだけで大丈夫のようです。

これで設定は完了!
まずはターミナルで下記コマンドを実行します。

npm run test-debug

今回test-debugと命名したので、runをつけなければ動きません。
npm上でエイリアスできないものはrunをつけないと動かない?というイメージです。
(ちなみにtstやtもtestコマンドとしてエイリアスされるので、被らないようにしなければいけないっぽい)
詳しく読んでないので文献を載せておきます。意味が違ったらすみません。
test | npm Documentation
run-script | npm Documentation

次にtestディレクトリ配下のjsファイルにブレークポイントを設置して・・・
VSCode上のデバック項目を確認

f:id:kinachan0725:20170721160056p:plain

launch.jsonで命名したRun mochaが反映されています。
再生アイコンを押して実行!

f:id:kinachan0725:20170721160059p:plain

ちゃんとES6上のコードが反映されています。
一件落着!