テストなし、ドキュメントなし、要件不明のソフトウェアに改修を加える場合、最初にリポジトリ単位でIOの正常系を確認する自動ブラックボックス試験を開発環境で動かそう


テストなし、ドキュメントなし、要件不明、単体テストは書けない、構成図もない、でも動いてる……。

そんなソフトウェアに改修を加え必要がある場合、最初に、リポジトリ単位でネットワークIOの正常系を確認する試験を書こう。
できれば異常系も拾えるだけ拾いたい。
そして、開発環境でいつでも動かせるようにしよう。
git push後のCIやデプロイはコストが高いので、ローカルで開発しているなら可能な限りローカル環境でテストできるようにしたい。

これが絶対的に正しいというつもりはありません。
私ならこうする(それ以外のいい方法がわからない)という話です。

できればこれ以上に良い方法を知りたい……。

想定するソフトウェア

  • UIがない
  • 何らかのネットワークIOを行うことを主とするソフトウェア
  • 外部との結合点さえはっきりすれば、モックを使ってリポジトリ単位で動かすことができる(と予想できる)
    • 言い換えれば、そのソフトウェアはDockerコンテナとして動かせる
    • (結合点が多すぎるとリポジトリ単位でテストすることは難しい。その場合は切り分ける)
  • 何をしているかソースコードから判断することが難しい
  • 結合テストがない
  • 仕様書、要件定義書、構成図の類のドキュメントがない
  • 単体テストがないand書くの無理そう
  • 作成した時に意図した通りの動作をしていると思われる
  • 関連するソフトウェアも含めてソースコードを読み書きする権限がある
  • 何らかの改修を加えたい

やること

  • 改修する前に、正常なネットワークIOを確認する。
  • そのために、本体のソースコードには一切手を加えず、外部とのネットワークIOをブラックボックス状態でテストできる状態にする。
  • 自動で実行でき、短く(長くても5分位)で終わるようにする。

なぜ試験するか?

  • 改修の結果がある程度正しいことを常に確認できるようにするため
  • テストがなければ、自分が加えた変更を確かめるために多大なるコストが必要になる
    • デプロイしてcurlしてみる、とか
  • リリースして一大事になって気づくリスクも減らせる

なぜ本体を書き換える前に試験するか?

  • 現在(書き換える前)の正確な挙動を確認するため

なぜネットワークIOを試験するか?

  • 少なくともネットワークIOが正しいことは必須要件である場合が多い
  • ネットワークIOは試験を行いやすい

なぜ自動化するか

(今回の自動テストの定義:コピペしなくても覚えられる程度のスクリプトを実行したら待つだけでテストが終わる状態)

  • 手動の場合面倒くさくて全部やらない可能性があり、テスト頻度も下がる
  • 改修の度に毎回実行することで問題の切り分けが容易
  • テストを共有することで、問題に気付きやすくなる

 なぜ単体試験より結合試験を優先するか

(たまに、リポジトリ単位のテストを単体テストと呼ぶ人がいますが、単体試験: unit Test、結合試験: integration testsであることを考えるとこの試験は結合試験なので、結合試験と呼ぶ。環境依存の問題が明らかにならないので「簡易な結合試験」くらいの呼称が正しいはず。すでに表記ゆれしてるのは大目に見てほしい)

  • 前提としてユニットテストをまともに書けない、がある。
  • 全体が正常に動くことが優先される
    • ユニットテストを書いたところで全体が正常に動く保証がない

具体的なやり方(私の場合)

私が上記のような状況で行う試験の書き方をご紹介します。

主な使用ツール

  • docker-compose
  • 型を明記でき、コンパイルと実行が速いコンパイル言語(私の場合はgo)

docker-composeについて

  • (私にとって)慣れたツールである
  • 移植性が高く共有が容易
  • 様々なソフトウェア(もろもろのDB、オンメモリストレージ、ミドルウェアなど)がだいたいコンテナとして配布されている
  • shellから実行できる
  • テスト結果の判定は自分で書く必要がある(結合テスト用コンテナがあれば誰か教えてください)

もっといいものがあるかはわかりませんが、今のところはこれで満足。

静的なコンパイル言語について

  • 議論の分かれるところだと思う
  • IOの型を明記したほうがよいため、型がわかりやすい言語がおすすめ
  • 動的型付けのスクリプト言語で書くと、IOの構造がわかりづらい
  • コンパイル言語で書くことで、インタプリタ言語で書いた場合に比べ、初歩的なミスがわかりやすくなる
    • 改修を加えた際に試験と本体のどちらが間違っているかわからない状況は笑えない
    • 今回の要件では、goは良いチョイスだと個人的には思う

基本

  • 「ローカル(かコンテナ)で立ち上げること」「正しいIOをすべて明らかにする」の二つのステップがある。

ローカルorコンテナでの実行について

  • すでにコンテナ(あるいはそれに類するもの)で動いている場合は話が早い
  • なければまず立ち上げるところからはじめよう
  • 多分何かしら問題が生じるのでひとつひとつ解決していく
  • ローカルで立ち上げても動作確認が困難な場合、すでに動いている環境でのIOの確認を先にするとよいかも

IOを明らかにするステップについて

  • とりあえず、IOしているところをソースコードからすべて洗い出す
  • わかりやすいところからIOの詳細を明らかにする
  • 型がわかりやすい言語なら中にIOの詳細が書いてあるかもしれない
  • すでに立ち上がっている環境やIO先のソースコードなど利用できるものは全部使う

作業

  • ひたすら地道に作業する
  • ソースコードだけではわからないし、実際に動いている環境だけでもわからない(実力のある人ならコード読んだだけで理解できるかもしれない。ただ、理解できてもテストは必須)
  • エラーが出たら嬉しい(エラーがでないことがある)
  • プリントデバッグも駆使(適当なところでなにかプリントする)
  • 最終的に変更前のソースコードで動くなら、別ブランチでどれだけ変更を加えても良い
  • あとは地道に作業する(大切なことなこと)

テストの作成

  • 型を明記する
  • シンプルに保つ

  • IO先(関連するソフトウェア)はすべてコンテナを用意する

ケースについて

  • まずは一番簡単な正常系で動かす
  • テストに含められる異常系は、issueに残してテストに含める(直すのはあと)
  • テストを続行できない異常系はissueに残してテストからは忘れる(テストを動かすのが先)

テスト結果判定ロジックについて

  • とりあえず想定される出力と実際の出力をすべてソートして、比べるくらいシンプルでよい
    • この方法だと異常系の判定が難しいが、まず正常系で動かすのが大切
  • テストケースの二重管理をしてはいけない
  • 結合試験のデータは最終的にすべてひとつのコンテナで管理する可能性が高い

テストの実行

  • うまいツールがなければ(私は知らない)、テスト結果の判定は自分で書く
  • 何度も言うが簡単に実行できて移植性が高いことが重要
  • 他の開発者に実行してもらえないなら、作る意義が薄れる
  • パフォーマンスはテストを実行できるなら忘れる(パフォーマンスチューニングを本番と違う環境で行うことは難しいため)
  • 開発と同じマシンで実行できることが大事

完成

  • 苦労の末に完成すれば試験があることがどれだけ幸せか改めて思い知るはず
  • 試験が完成したら改修に移ろう

その他

  • 上のやり方はあくまで私がやるとしたらです。
  • テストがないことを許容する開発者の中でテストを作っても評価されない可能性がある
    • 試験の必要性をわかっていない人も多い
  • 仕様書とテストが食い違っていた場合、テストが成功しているならテストの方が信用できることが多い
  • テストはメンテナンスコストがかかるので、必ずつくれば良いというものではない、と心に留めておく
    • でも必要なことが多い
  • 結合試験が複雑になるなら結合試験の単体試験も必要かも

まとめ

  • 試験は大切
  • 中身のわからないものを動かす際には試験必須
  • 試験を作るのは大変
  • がんばる