この記事は Akerunのカレンダー | Advent Calendar 2022 - Qiita の 14日目の記事です。 はじめまして、nodeJS初心者の Esperna - Qiitaです(2022年6月からnodeJS触り始めました)。
背景
自動テストがない(手動の実機テストのみの)レガシーコードで、ちょっとした修正をするのにもバグが出やすく、 リファクタリングをするのが大変という状況ってありますよね。 そのような状況で今年自分が取り組んだことを書いてみます。 nodeJSにはJest*1があるのでそのチュートリアルを見ながら 次の2つを意識して単体テストを書きました。
やったこと
実際にやったことは以下です(いわゆるテスト駆動開発)。
- 主要なロジックや過去に不具合が出た箇所に関して、仕様を調べた
- 上記を元に単体テストを書くことで既存の振舞いが変わったを検知できるようにした(ソフトウェア万力)*2
- 現行のソフトの振る舞いを明らかにした
- 後述のCIと組み合わせることで既存の振舞いが変わった場合にテストがFAILするようにした
- 新規ソースコードを書く際は失敗するテストコードを書いてからそれをPASSさせるようにした(Red Green Refactor)*3
- 最低限のCI(Continuous Integration)ツールを設定した
CIの設定抜粋
image: node:xx.x.xx stages: - lint - test cache: key: files: - package-lock.json paths: - .npm/ lint: before_script: - npm ci --cache .npm --prefer-offline stage: lint script: - npx eslint src test: stage: test script: - npm ci --cache ../.npm --prefer-offline - npm run test test
- Jestを実行しカバレッジを出力した
- カバレッジは数値そのものよりもどのパスが通ってないか知ることを意識した。通ってないパスにバグが潜んでいる可能性があるため
- 依存関係が複雑でテストが書きにくいものに対してJestによりmockを設定し、test及びmockフォルダをsrcと分離した(テストダブルによる依存関係の分離)*5
- 単体テストを書くためにrewire*6の
__set__
を使ってconst(再代入できない変数)を書き換えた - nock*7を使ってhttpレスポンスをmockした
テストとソースコードの典型例は以下です。
class Sample { constructor() { this.client = null; } method1() { const res = { isSuccess: false, body: "", }; if (this.client === null) { res.isSuccess = false; return res; } res = doSomething(); return res; } method2(a, b) { return a + b; } } module.exports = new Sample();
テストコード
const target = require("./sample"); describe("Sample Class test", () => { //Single function test test("func1 shall return when client is null", () => { target.__set__({ client: null }); const result = target.method1(); expect(result.isSuccess).toBe(false); }); //Parameterized test it.each([ ["method2 shall return sum if A,B are default", 10, 10, 20], ["method2 shall return sum if A,B are more than default", 20, 20, 40], ["method2 shall return sum if A,B are less than default", 5, 5, 10], ["method2 shall return sum if A is more than default and B is less than default", 20, 5, 25], ["method2 shall return sum if A is less than default and B is more than default", 5, 20, 25], ])("%s", (testcase, A, B, want) => { const result = target.method2(A, B); expect(result).toBe(want); }); });
テストの結果の例は以下です。 Uncovered Lineから通ってないパスが分かると思います(もちろんvisualizeすることも可能です)。 今回の場合はclientがnullでない場合のreturn resです。ここでは書きませんが実際はここのパスを通すテストを追加していきます。
% npm test > test > jest --runInBand --coverage PASS ./sample.test.js Sample Class test ✓ func1 shall return when param is null (1 ms) ✓ method2 shall return sum if paramA,B are default ✓ method2 shall return sum if paramA,B are more than default ✓ method2 shall return sum if paramA,B are less than default ✓ method2 shall return sum if paramA is more than default and paramB is less than default ✓ method2 shall return sum if paramA is less than default and paramB is more than default -----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------|---------|----------|---------|---------|------------------- All files | 87.5 | 50 | 100 | 87.5 | sample.js | 87.5 | 50 | 100 | 87.5 | 15 -----------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 6 passed, 6 total Snapshots: 0 total Time: 0.572 s, estimated 1 s Ran all test suites.
今後の課題
- 現行のソフトの振る舞いが明らかになったがエラーチェックなど仕様として足りてない部分がある
- テストの書き方が洗練されていない
- テストコードが長く複雑で読みにくくなってしまうことがあった
- テストの量が増えてくると実行時間がボトルネックになるのが目に見えているので並列化なども追々考えたい
所感
こうして書いてみると、当たり前のことばかりで、技術的に地味ですね。 でも、当たり前のことを当たり前にやることが難しく、 一見地味でもやっていくと効率が上がると思うので続けていこうと思います。
参考文献
jestjs.io www.shoeisha.co.jp shop.ohmsha.co.jp yshibata.blog.ss-blog.jp xunitpatterns.com www.oreilly.co.jp github.com github.com
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
*2:「レガシーコード改善ガイド」に> 変更を見つけるためにテストを用意することは、コードを万力で固定するのと同様の効果があるという記載がある
*3:「テスト駆動開発」にRed Green Refactorに関する記載があるが、「テスト駆動開発による組み込みプログラミング」で知った
*4:「テスト駆動開発」にテストファーストに関する記載があるが、そちらの内容よりは柴田芳樹さんのブログに記載されている内容を意識している
*5:「xUnit Testing Pattern」で提唱された用語だが「テスト駆動開発による組み込みプログラミング」で知った
*6:nodeJSの単体テストにsetter/getterを追加するパッケージ
*7:nodeJSでhttpサーバをmockするライブラリ