UnitTest を書くモチベーションがなかなか上がらない。それはなぜかを日々悶々と考えていました。
なぜこうもUnitTestをめんどくさく感じるのだろう。。。
Testが重要なことは十分理解しているし、品質を保つために必然であることもわかっているのに。
そして、今回のPostはこの日々の悶々とした感情に終止符を打つための私からのひとつの提案です。
テストフレームワークは jest、言語は node.js、 AWSのlambda、APIGatewayがベースとなるコードを使っていますが、「考えかた」の提案なので、他の言語にも応用できると思います。
なぜモチベーションが上がらないのか?
以下のコードを見てください。
describe('some component', () => {
beforeEach(() => {
ModoleA.func = mock.fn().mockImplementation(() => {
.....
.....
})
ModoleB.func = mock.fn().mockImplementation(() => {
.....
.....
})
ModoleC.func = mock.fn().mockImplementation(() => {
.....
.....
})
})
it('some test', async () => {
ModoleD.func = mock.fn().mockImplementation(() => {
.....
.....
})
ModoleE.func = mock.fn().mockImplementation(() => {
.....
.....
})
ModoleF.func = mock.fn().mockImplementation(() => {
.....
.....
})
const result = await target.func(dummyArg1, dummyArg2, dummyArg3)
expect(result).toBe(xxxx)
})
})
このコードでテストしたいことは、最後の2行でしょう。あとは全て外堀を埋める作業です。
往々にしてこのようなコードが散見されるのには、さまざまな理由があると思います。実際のテストコードは複数のMockライブラリを参照したり、複雑なロジックをテストするでしょうから、さらにMockingのコードは複雑でしょう。Mockが増えれば増えるほど、何が確認したいのかがどんどん埋もれていってしまいます。
もっと、 「確認したいこと」 に集中できる(可能性のある)コードが必要です。
一つの考えかた
ベースとなる考えかたとして、以下の本が参考になります。
The Art of Readable Code
https://www.amazon.com/Art-Readable-Code-Practical-Techniques-ebook/dp/B0064CZ1XE
中心となる考えかたは、「Code As A Document」 です。
テストの仕様書の要素を想像してみましょう。
- テストケース
- 説明
- 入力
- 前提条件
- 期待する結果
- 実施結果
- 判定
このくらいでしょうか。あとは、ナンバリングやカテゴリなどもあるかもしれませんね。
それでは、これらの要素を踏まえて、テストコードをテスト仕様書としても扱えるように構成することを考えてみます。
いきなり答え
いきなりですが以下のようなフォルダ構成を検討してみてはどうでしょうか。 (私が実戦投入しているコードとほぼ同じです。)
__tests__
__mocks__ <-- Mockを定義するフォルダ
__testCase__ <-- テストケースを定義するフォルダ
moduleA.js <-- moduleAのテストケース
moduleA.js <-- moduleAのテストファイル
moduleB.js
:
testN.js
moduleAのtestCase例
テストケースの構成が一番重要な部分です。わたし達はこのケースを作ることにFocusしたい。
module.exports = {
funcXxxNormalSequence: { <-- テストケースの名前。ケースが想像しやすいようにしておく。
event: { <-- テスト引数に入れる引数。名前はargsとかでもよいかも。
arg1: xxxx,
arg2: xxxx
},
mock: { <-- メソッド内でMockするファンクションの返却値
mockFuncA: [],
mockFuncB: true,
mockFuncC: {},
mockFuncD: 'dummy'
},
expects: (result) => { <-- 検証コードと結果の確認
expect(result).toBe(true)
expect(result.statusCode).toBe(200)
:
}
}
}
moduleAのテストファイル例
const ModuleBMock = require('../__mocks__/moduleB')
const ModuleCMock = require('../__mocks__/moduleC')
const ModuleDMock = require('../__mocks__/moduleD')
const ModuleA = require('../../../moduleA')
const TestCase = require('../../__testCase__/moduleA')
describe ('moduleA', () => {
beforeEach(() => {
jest.restoreAllMocks()
})
const mock = (testCase) => {
ModuleBMock.doMock('funcA', () => Promise.resolve(testCase.mock.mockFuncA))
ModuleCMock.doMock('funcB', () => Promise.resolve(testCase.mock.mockFuncB))
ModuleDMock.doMock('funcC', () => Promise.resolve(testCase.mock.mockFuncC))
ModuleBMock.doMock('funcA', () => Promise.resolve(testCase.mock.mockFuncD))
}
describe('[method] target funcXxx', () => {
it ('should be succeeded normal sequence', async () => {
const testCase = TestCase.funcXxxNormalSequence
mock(testCase)
const result = await ModuleA.funcXxx(testCase.event)
testCase.expects(result)
})
})
})
mockの例
const Target = require('../../../modoleB')
module.exports = {
doMock: (method, func) => {
jest.spyOn(Target, method).mockImplementation(func)
}
}
# クラスインスタンスをMockするなら、
# jest.spyOn(Target.prototype, method).mockImplementation(func)
# とします。
どうでしょうか?
上のコードで、mock, テスト実行のコードはパターン化されています。
そして、プログラマーはもっとも重要なテストデータとテストパターンを作ることに集中できます。
この構造のメリットは以下のようなものです。
Pros/Cons
Pros
- テストケースがInput/Outputベースになり、メソッドの仕様に集中できる
- mockとテストコードがどのテストにおいても同じパターンになり、頭を使わなくてよくなる。
- Mockの戻り値をテストケースでいっしょに考えることができ、メソッドのロジックによりFocusできる
Cons
- 規模が大きくなるとMockのrequireが増える。(そうなった時点で対象のメソッドも不穏な匂いがしているとも言える)
Tips(node.js/jest)
-
mock メソッドには、jest.spyOn(object, method) がオススメです。jest.restoreAllMocks()が利用できるので、テストの度にMockをクリアできます。
-
module mockはむやみに使わない。Immutableにテストしたいケースがほとんどなので、テストの前には最初に綺麗な状態に戻したいのです。jest.mock(modulename)でライブラリ全部をモックすると、モジュール内の一部の関数だけMockingしたいときに意図せずMockしたくないmethodでMockが呼ばれます。
#さいごに
普段使い慣れている node.js, AWS lambda, jest をベースに説明を進めましたが、最初にも言ったようにこれは考えかたの提案です。特性の違いはあれど、他の言語、テストフレームワークでも流用できるものではないかなあと思ったので、Postしました。
このPostがテストに悶々としているプログラマーの考えかたの一助になれば幸いです。