超短納期開発Tips vol.1 - JEST駆動開発で辛い試行錯誤フェーズを素早く心穏やかに終わらせる


はじめに

今年もヒーローズ・リーグに参戦させて頂きました。例年通り素敵な体験をさせていただけた思う一方、戦績としては決勝ステージには進出しつつも、何も賞を獲得することが出来きませんでした。ここ数年何かしらの賞を頂いていたことが、いつのまにか自分のアイデンティティになっていたんだなぁと思ってしまった次第です。
この寂しくも悔しい気持ちを沈めたいという想いをこめつつ、散っていったアイデア達を作るうえで学習したことを言語化してアウトプットすることで、次の挑戦への糧とすると共に、来年の締め切り当日になんらかのアイディアを思いついてしまう多くの方が諦めずに応募まで進める助けにができば幸いです。

超短納期開発Tipsって何?

ヒーローズ・リーグ応募やハッカソン参加など、比較的短時間でアイディアを練ってプロトタイプを仕上げる必要があるときの開発Tipsです。ハッカソンTipsと読み替えても差し支えありません。

具体的な規模と速度間としては、こういうものこういうもの を数時間で実装することを想定しています。シリーズ展開しつつ、この手のイベントへの挑戦者増加に寄与できたら嬉しいです。
 
(フロントエンドネタや開発プロセスネタなど色々構想はありますが、第1弾で終わる可能性も十分あります)

また、筆者の得意領域がWebなので、フロントはWebで実装できるものはWebで実装しがちであり、Node.jsが好きなのでバックエンドはNode.jsで実装しがち。それゆえにTipsもその辺りみかたよりがちです。

この記事で解決したい課題

触ったことのないAPIを実行するコードの実装や、少し複雑なロジックの実装にかかる試行錯誤の時間を短縮したい

ハッカソンにおける開発やヒーローズ・リーグなどのコンテスト締め切り直前に思い立って作り始めてしまった場合、3~6時間という短い時間で動くプロトタイプを仕上げる必要があります。加えて、こういった場合に限って「新しい触ったことのないAPIを利用する」というケースが多くあります。

競技の特性的には「どう見せるか」「DEMOに耐えられるか」「展示に耐えられるか」などの課題にリソースを割きたい一方で、癖の強いAPIを利用する必要があったり、実装しようとしているアイディアによって色々なAPIを組み合わせて少し複雑なことをする〜などが要求されていたりすると、ある程度実装しつつ動かしつつ正解を模索していく作業、いわゆる試行錯誤の作業にも時間をかなり奪われることになります。

本記事ではこの試行錯誤の作業を効率よく、ストレスフリーに、(3~6時間という短時間において)メンテナブルに構築する開発フローを紹介します。余談ですが、最近の筆者と同じように「ElasticSearchのクエリを投げるのも正直毎回ググってばかりで苦痛を感じている」という方にも多分効くはずです。

JEST駆動開発 - 概要

JEST駆動開発とは Jinriki-tEST-Driven-Development、つまり人力テスト駆動開発のことをさします(本当にすみません。でも、JESTと大文字表記していたので何かしらの嫌な予感のようなものは察していただけたとは思うのです)。

ポイントは以下の二つです。

  1. テスティングフレームワークとWatchツールを単なるメソッド自動実行機として使い、コンソールデバッグを最大限に効率化する(この場合はJest x watchオプション)
  2. テストコードと実装を同じファイルに書きつつ、なんならそのまま利用箇所にインポートして利用してしまうことで、作成するファイルやディレクトリの数、実装中のファイル移動の機会、実装/実装中断のスイッチングコストを削減する

自動テストによる安心感を感じることはできないものの、コードを書いてからフィードバックを得るまでの時間と手間を短くすることと、コードを 利用する側 から書くことで得られるメリットは享受する、という主な狙いがあり、加えて(2)の要素によって(1)を行う際に発生する手間を軽減し、短時間の開発でもペイするようにする、というのがお気持ちです。

ここまでで「ああ、そういうこと...」と理解されたかたはこの先は読まないで良いと思います :)

コンソールデバッグのつらみ

詳細に入る前に少し課題に戻ります。

(このレベルで)急いでプロトタイプを組むとなると、綺麗な土台を整える恩恵を得る時間がとても短いこともあり、なんだかんだでコンソールデバッグに頼ることが現実です。とりあえずAPIリファレンスを読んで、RESTクライアントやCURLなどで実行してみた後は、コードから実行してみよう!ちゃんととれるかな?というタイミングなどは特にそうですね。

また、開発初期段階から見せ方についてはケアしていきたいため、全体として以下のような順で開発をすることが多いことも、開発を非効率にしやすい力学が働きます

  1. まずはダミーのデータで良いからフロント向けのWebAPIを実装してフロントエンド側が動作確認できるようにしておく
  2. 実装の中身をダミーデータからちゃんとAPIにアクセスしたり、真面目に計算したりするようにする

このとき、「修正するたびに毎回アプリを再起動してAPIを叩いて確かめる..」ということをしている人をよく見かけます。または「app.jsにとりあえずAPIコールを実行するようなコードを書いてコンソールに出力しつつ、コード修正->app.js再起動で確認...を繰り返したのち、固まったらメソッド化してHttpリクエストハンドラから見えるところに移動させて~~」ということをする人も見かけます。普段そうしない人でも、なぜか時間制限がつくとそうする人が多くいます。全部やったことがあります。

おそらく「焦りは余計な緊張を生み、実力を半減させる」的なやつなのでしょう。

そして、終盤になればなるほどやり方を改善するための時間も心の余裕も減っていくので、TypoやHeader要素の追加漏れ、Credentialを環境変数で渡さないといけなかったぜ!などの変更をするたびに同じ事を繰り返すことになります。変更してみて、間違っていたら途中の箇所にconsole.logを仕込んで、直して..の繰り返しです。 辛い。

今回の提案はこれをやめよう!というものです。
終盤での「見せ方にこだわりたい!」という欲求からの変更は経験上不可避なので、備えておいて損はないでしょう。

JEST駆動開発における試行錯誤の流れ

実際の作業のイメージをまずは文字ベースで列挙します。

  1. jest --watchAllを走らせて、保存するたびにテストコードが実行されるようにする
  2. 試行錯誤のためのコード(メソッド)と、それを実行してくれるテストコードを同じファイルに記述する
  3. とりあえず結果が得られるようにする。結果の確認はコンソールデバッグで良い(アサーションを頑張らなくていい)
  4. 動くようになったら、諸パラメータを引数化したり設定ファイルに切り出したりする
  5. 一通り満足したらdescribe.skipとして実行をスキップするようにする
  6. そのままmodule.exportsして、利用側から読み込んで利用する
  7. 次の試行錯誤にうつる

基本的にはこの繰り返しdえす。再び修正したくなったらdescribe.skipを外して、テストが動く状態で開発します。テストコード側に実装まで書いてしまうのはマストではありませんが、ディレクトリを行き来したり、試行錯誤途中の変更箇所を最小限にとどめたいという理由でそうしています。ディレクトリ構成を後から変えたいと思った時など、移動するファイルが二つにならずに済みます。

実際のコードイメージ

上記のフローで開発したコードのサンプルを貼っておきます。
GET : /issueTitles?repoName=<リポジトリ名>で筆者所有の公開リポジトリから、指定したリポジトリに紐づくIssue名の配列が取れるというあまり面白くないAPIですが、そこは勘弁してください。エラー処理?この環境下ではそんな余裕はありません。コンソールに出して500で返せば良いです。

./__tests__/github.test.js
const axios = require('axios');
// リポジトリ名をうけとり、リポジトリに紐づくIssueのタイトルの配列を返す
async function getIssueTitles(repoName){
  const { data }  = await axios.get(`https://api.github.com/repos/rockymanobi/${repoName}/issues`);
  return data.map(issue => issue.title);
}
module.exports = { getIssueTitles };

// 実行側から読み込まれるときにエラーにならないように環境変数を参照する
// ネストは増えるが、実装側にコピーする時間が惜しい
if(process.env.NODE_ENV === 'test'){
  describe('hoge', ()=>{
    it('hoge', async () => {
      const issueTitles = await getIssueTitles('dm-keisatsu');
      // コンソールデバッグで良い
      // めちゃくちゃ余裕があればAssertionにすれば良い
      console.log(issueTitles);
    })
  });
}

このファイルを保存するたびに、テストが実行されて、コンソールに結果が出力されます。

利用側のコードのイメージはコチラ。

./app.js
const express = require("express");
const app = express();

// テストファイルから実装を読み込んで利用
const { getIssueTitles } = require('./__tests__/github.test');
// GitHubより、指定されたリポジトリに紐づくIssueを取得する
app.get('/issues',  async (req, res, next) => {
  const response = await getIssueTitles(req.query.repoName);
  res.send(response);
});

const server = app.listen(process.env.PORT || 3000, () => {
  console.log("listening on PORT:" + server.address().port);
});

何が美味しいのか

出来上がったコードは別に綺麗でも何でもなく、ポイントは 「試行錯誤が必要だったであろうコード(この場合はGitHub APIの呼び出し, Issueタイトルの抽出等)を実装もろともテストファイルに書きつつ、保存と同時にコンソールデバッグで結果を確認できる状態を担保しながらコードを書いていった結果である」 というところです。

ファイルを保存->実行を手動でやる場合よりも確実に試行錯誤は素早くでる上に、お試しコードをコメントアウトしたりそのためにimportしたりなどの作業も不要です。また、この例ではメソッド実行箇所が一つだけですが、複数の実行パターンを同時に試すこともできます(当たり前ですが)。そして、もし実装が固まって余裕があれば、アサーションを書いて本当にテストコードとして運用することも可能です。

変更してみる

変更したくなったときには特に恩恵を感じます。

筆者以外のユーザのリポジトリを対象とできるよう、試しにgetIssueTitlesの引数にuserNameをとるように修正するケースを考えます。メソッドの引数を加え、URL生成時に利用するようにしつつ、メソッドの呼び出し元からユーザ名を渡すようにする...このケースだと単純かもしれませんが、コードを実行している間は常に同じ結果が出力されながら作業ができるので、手動で実行する必要があるのはAPI経由で呼び出してみて動くかどうかを確かめたいときだけになります。

基本的にdescribe.skipで実装中のもの以外はテストを実行しないようにしていれば、確認が目視であってもさほど困りません(2日後以降は困るかもしれませんが)

違う呼び出しパターンを確認する

あたりまえですが、違う呼び出しパターンを確認したい時でも、エディタ上でコードをコピーするだけなので簡単です。この例は単純なものですが、引数が複雑な場合などは、やはりコード上にあるものをカジュアルにいじれて即時実行されるというのはとても便利なものです。

  describe('test', ()=>{
    it('hoge', async () => {
      const issueTitles = await getIssueNames('rockymanobi', 'dm-keisatsu');
      console.log(issueTitles);
    })
    // リポジトリ変えても大丈夫かな取れるかな...
    it('hoge', async () => {
      const issueTitles = await getIssueNames('rockymanobi', 'thanks-to-leave-bot');
      console.log(issueTitles);
    })
  });

その他の美味しいところ

普通にテストを書くことによるメリットに近いものがえられることがあります。変更したときの安全性はもちろん担保されませんが、あまり考えなくても(大事) コードが自然に、テスト可能な単位で分割される力学を得られます。

まとめ

まとめると...

  • Jest --watchAll環境で作業することで、実行を完全に自動化、弄りやすいコード化しよう
  • アサーションを書いたりするのは試行錯誤段階ではつらいので、とりあえずコンソールデバッグで作り込もう
  • すごく時間もないので、作成するファイルを削ったり、実装やめて他に行ってもう一度戻ってきたりしたときに楽なようにテストコードと同じファイルに実装も書いてしまうというてもあるよ

となります。以上です!

諸注意

ここまでの内容はあくまで手元の試行錯誤と、(ものすごく短いという意味での)短期間での変化への対応力を重視した開発方法であり、これをプロダクションコードにそのまま載せることは多分許されないので気をつけましょう。

また、すでにお分かりかとは思いますが、このエントリは半分はネタで書いています。 特にタイトルは完全にネタ であり、単純にテスティングフレームワークをコンソールデバッグ最適化マシンとして使いましょういうだけの話で、目新しい話は何一つありません。本気な部分は、実際に筆者が手元の試行錯誤にこのフローを実践しているというところと、超短納期な開発であってもこのレベルの整備であればメリットの方が大きい、というところでしょうか。

それからjest --silent=trueとなっているとコンソールに何も吐かれないので注意しましょう。

終わりに

やってみると意外に使えると思います。
普段、テスト書くのはちょっと、、、とハードルを感じている方も、何かUtil的なものを作るときに試しにやってみて、勢いでちゃんとアサーションを書いて、最後に実装コードをちゃんとした場所に持っていけば、それがテストを書く第一歩になったりもするのかなぁ〜とか期待していたりもします。

以上です。来年の締め切り当日などになったら思い出して頂き、是非お試しあれ。

それでは。