モジュール内で呼び出されてるメソッドをスタブ化できるproxyQuireが便利すぎた。
テストを書いて困ったことがあったので、備忘録して書いておきます。
実装側はこんな感じ。
//Goods.js テスト対象 import {readFileSync} from 'fs'; const goodThings = JSON.parse(readFileSync('/rankValues/good')); const sosoThings = JSON.parse(readFileSync('/rankValues/soso')); const badThings = JSON.parse(readFileSync('/rankValues/bad')); export default class Goods { get (status) { let goods; const goodsList = [goodThings, sosoThings, badThings]; goodsList.forEach((key) => { if (status === key) goods = goodsList[key]; }); return goods; } }
こんな感じのモジュールがあって、
無心でテストを書いてたらイケてないことに気付きました。
(途中で気付いたのですが、最後まで無心ならこうなってただろうという想定で書きました)
//BadTest.js イケてなかったテスト import 'mocha'; import assert from 'power-assert'; import {readFileSync} from 'fs'; import Goods from '../../src/Goods'; const goodThings = JSON.parse(readFileSync('/rankValues/good')); const sosoThings = JSON.parse(readFileSync('/rankValues/soso')); const badThings = JSON.parse(readFileSync('/rankValues/bad')); describe('テスト.だめな例', () => { let goods = new Goods(); }); it('Good!', () => { const result = goods.get('good'); assert.deepEqual(result, goodThings); }); it('Soso!', () => { const result = goods.get('soso'); assert.deepEqual(result, sosoThings); }); it('Bad....', () => { const result = goods.get('bad'); assert.deepEqual(result, badThings); }); });
このテストケースの問題点としては、実装されたデータを使用しているという点ですね。
テストケースのデータは定数(enumとか?)以外は
テスト用のデータを使うのがいいですよね。。
例えば
あくまでテストのデータの中身を検証するテストではなくて、get()の実装を見るテストなので・・・
ただし変更が困難なのがconstで定義されたgoodsがclassの外側に出ていること。
これはimportで読み込まれた時点で処理がスタート(この場合はfs.readFileSync()とJSON.parse())してるため
うかつに変えることができません・・・
また、今回のソースとは異なりますが
//hoge.jsのimport文 import Foo from 'foo'; // foo.jsのimport文 import Bar from 'bar';
のように複雑にimportされているテストだと、
hoge.jsを成功させるにはFooとBarの処理を想定してテストを書かなきゃいけません。
これじゃ単体テストの意味がありません・・・。
そこで必要になるのがproxyQuireです。
簡単にいえばimportなどで呼ばれたモジュールやexport以外のモジュールや
関数などをスタブ化出来るというものです。
sinon.jsと組み合わせで実装していきます。
//Test.js import 'mocha'; import assert from 'power-assert'; import proxyQuire from 'proxyquire'; import sinon from 'sinon'; import Goods from '../../src/Goods';; // この3行はテストデータです。実装側のファイルは使用しません(読まない)。 const goodThings = {good: {goods: ['tv','tablet','guitar'],}}; const sosoThings = {soso: {goods: ['fruit','meet','juice'],}}; const badThings = {bad: {goods: ['shit','paper','trash'],}}; function createProxyQuire() { // スタブ化していきます。今回スタブ化するのはfs.readFileSyncのみなので // 3種類の引数が渡されたらテストデータを返すようスタブを作成します。 const stubReadFileSync = sinon.stub(); stubReadFileSync.withArgs('/rankValues/good') .returns(JSON.stringify(goodThings)); stubReadFileSync.withArgs('/rankValues/soso') .returns(JSON.stringify(sosoThings)); stubReadFileSync.withArgs('/rankValues/bad') .returns(JSON.stringify(badThings)); // ProxyQuireを使用します。 // 第一引数にテスト対象のモジュール(相対パス), // 第二引数にスタブ化したいものをkey-value形式でかいていきます。 const proxyQuireAuthService = proxyQuire('../../src/Goods',{ fs:{ readFileSync: stubReadFileSync } }); return proxyQuireAuthService; } describe('proxyQuireを使ったテスト', () => { const Goods = createProxyQuire(); }); it('Good!', () => { const result = Goods.default.prototype.get('good'); assert.deepEqual(result, goodThings); }); it('Soso!', () => { const result = Goods.default.prototype.get('soso'); assert.deepEqual(result, sosoThings); }); it('Bad....', () => { const result = Goods.default.prototype.get('bad'); assert.deepEqual(result, badThings); }); });
本日はこんな感じです。
はじめてつかったGitのリベースとスカッシュ
社内で新しいGitの運用方法が採用されたので
忘れないようにやり方をメモリます。
今回はGitExtensionを使ったやり方を記述しています。
リベースとスカッシュの手順
経緯:masterブランチやdevelopブランチからトピックブランチを作成して
developやmasterにコミットしていく方法で運用していたのですが
複数回コミットを行った際に履歴が訳分からなくなるというのが今回の発端でした。
まず、こんな感じのブランチがあって、ここでトピックブランチを作成します。
適当に2回コミットしました。
これらのCSSファイル、JSファイルの追加を1コミットにまとめたい。。
コマンドラインで下記のコマンドを実施します。
git rebase -i HEAD~x
※xはまとめるコミットの数だけ指定します。
今回は2コミット分をまとめるので、git rebase HEAD~2とします。
なんかファイルが出てきました。
これを編集していきます。
内容を編集するためのテキストが開くので、まとめたいコミットがまとまるように書き換えます。
やり方の例としては最初のコミットをpickのままにして、
他のコミットをpick→squash(s)にすることでコミットが一つにまとまります。
そのため、編集箇所は2行目の「pick」を「s」に変更したらOKですね。
保存して閉じます。
閉じたらまたファイルが出てきました。
これはコミットの際に表示するメッセージですね。
今回はCSS&JSファイルを追加と変更します。
保存して閉じてF5を押すととメッセージが変更されて、コミット内容が1つになっているのが分かります。
これが「スカッシュ」の手順となります。
今度はリベースのテストです。
マスターブランチから新しくmenu.jsを追加したとします。
そのため、TOPIC_1のブランチはmenu.jsを取り込んでいない状態となります。
GitExtensionではこのような画面になります。
これを最新のブランチが分岐元になるように最新を取得します。
TOPIC_1のブランチで下記のコマンドを実行します。
git rebase '元となるブランチ'
今回はマスターブランチの最新から分岐させたいので、git rebase masterと打ちます。
うまくいったようです。
F5を押して更新を掛けてみます。
成功ですね!やった!
最新のメニューが追加された状態でマージを下ということになりますね。
もし、この際に競合が発生していたら解決をするという流れです。
そしてTOPIC_1のブランチをpushすればremote側も反映されるという仕組みです。
JavaScriptのES6で登場した{並括弧}をつかって代入(const {foo} = this)
なんか当たり前に覚えていたけど、それといった文献を見当たらなかったし
そもそも記号の検索って難しいので書きました。
日本語ですら記号の読み方が曖昧だったりするのに更に日本語のサイトがないとね・・・
初心者には厳しい時代になってきました。。
さて、掲題です。
ES6だとこのような記述が多く見られます。
import express from 'express'; import {readFileSync} from 'fs';
これ!なんで命名された変数を囲ってるの!
オブジェクト?オブジェクトなのかっ!?
って最初思って意味不明でした。。
なんとなく直感的に分かったのですが、慣れるまで時間が掛かりました。
以下のような感じがわかりやすいかと思います。
const test = {foo:'foo',bar: 'bar'}; console.log(test); //そのままobjectが出力されます。 //今までのやり方 const foo = test.foo; console.log(foo); const {bar} = test; //testObjectのbarのみを取り出す console.log(bar);
//出力結果 { foo: 'foo', bar: 'bar' } foo bar
こうやって簡単にすると分かり易いですね。
ちなみにこれ、import文でも可能ですし、関数でも可能です。
import {readFileSync} from 'fs'; const data = readFileSync('path');
上記のこの1行はreadFileSyncのみ使用可能にするということですね。
追伸:いい加減Expressのドキュメント覚えないとダメな気がしてきた。。。
何でも出来ちゃう反面、ちゃんと覚えないと効率が悪い。。。
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でテストの網羅率を調べたいという際に便利です。
使い方やインストール方法はこちら↓
//テスト対象メソッド 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を実行した場合このような結果が表示されました。
(必要のないテストはマスクかけました。)
各画面の見方です。
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の関数を削除した場合、値はどう変化するでしょうか。
試してみます。
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); }); });
んんん~~・・・
なぜStmtsが100になるのだ・・・
(今回の場合だとfalse,trueのパターンが存在しないためBranchが94%になっている)
それぞれ渡された引数がtrue,falseで存在するからかな。
まだまだ検証が必要のようです。
追記:パラメーターは関係なくそのままの命令網羅のようです。