こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問6. テストコード部分の品質はどのように維持するのでしょうか?テストコードのテストコードを作るのではないと思いますが……

ひとつは、プロダクトコードと同様のレビューとリファクタリング。もうひとつは、テスト駆動開発に特有ですが、プロダクトコードとテストコードがお互いに支え合うような関係にする考え方です。

テストのレビュー

まずはレビューですが、これは普通のやり方です。テストコードの書き方、テストの内容、他のテストと見比べた上で全体を把握しやすく書けているかなどを見ます。特に、テストコードや実行結果から、テスト一覧を出力したり、失敗時の表示などを見て、どんなテストをしているか把握できると、テストドキュメントをある程度代替できるので、そうした観点で読めるか・伝わるかも見ます。

プロダクトコードのレビュー時に、TDDで書いたテストコードも一緒にレビューするのも一般的です。急いでいるときはテストだけレビューすれば、何が動いているか、どこまで進んでいるかわかるというメリットもあります。

極端に品質の低いテストコードには、以下のようなものもあります。

  • テストの実体がない(ガワだけ。書き忘れ、時間不足)
  • テストが実行されていない(タイプミスなどで、けっこうやりがち)
  • なにも検証していない(assertがない。テストを理解できていない。コードカバレッジを増やすためにやってる場合もあるが、無意味)
  • テスト対象のプロダクトコードが、なぜかテストコードにコピーしてある(テストへの理解不足。「このコードをテストしたいんだ」という気持ちはわからなくもない。コピペ開発のやりすぎかも)
  • 書いた人の環境、PCでないと実行できないテストになっている(ファイルアクセスが必要、特殊なテストデータが必要などで、全員が実行できるようなテスト環境を整えられていない場合に見られる。開発ができる人ほど、できちゃうので、やりがち)

メンバーの理解不足、スキル不足が原因の場合もありますが、時間がなくてつい…ということもあります。人の目によるレビューは、だいたいいつも有用です。ペアプログラミングをしていれば、レビューは減らしても大丈夫かもしれません。ペアプロでも、コードレビューをまったくゼロにするチームは少ないようです。

テストコードもリファクタリング

レビューからのフィードバックを取り込むのは、テストコードのリファクタリングです。もっとも、レビューがなくても、リファクタリングではいつでもテストコードを対象にします。ここでは「コードとしてのリファクタリング」と「テストとしてのリファクタリング」を両方とも意識する必要があります。

テストコードもコードなので、プロダクトコードと同様、読みやすく、重複なく、変更しやすいようにリファクタリングをします。基本的にはプロダクトコードのリファクタリングと同様です。ただしテストコードの場合は、「どんなテストケースなのか」を読み取れるのが最優先となります。プロダクトコードは、コードを書く人しか通常読みませんが、テストコードを読む人はテストの内容を知るために読む場合が多くなります。そうするとロジック(How)を把握するより、何をしているのか(What)を把握するのが早いほうが、嬉しい。

私は、読む人は以下の順にテストコードを把握したいだろうと想定して、できるだけ早く必要な情報がわかるように意識しています。

  1. どんなテストか(What) - テスト名(メソッド名など)を見ればわかるように
  2. 確認している内容は何か - assert部分を1行だけ見ればわかるように
  3. どうやってテストしているか(How) - テストメソッド全体(だけ)、特にassertまでの準備部分を見ればわかるように
  4. テストを書き足すにはどうするのか - 共通化されたセットアップ、フィクスチャやヘルパーを含めたテストコード全体を見る

また、少しずつ異なる複数テスト内容が、少しずつ異なる複数のテストメソッドで実装されます。この「少しずつ」の差異は、テストコードの中に埋もれてしまって読み取りづらくなりがちです。「あれ、このテストとこっちのテスト何が違うんだろう…(5分後)あー、ここが123546789か123465789かの違いかー」みたいになります。この間違い探しはツラいし時間を食いますし、さらに違いを見落とす場合もあります(実はさらに"あああああああああ"と"ああああああああああ"の違いがあった、ということもある。なお、こんなテストを書いた人は呪われてしまえと思う)。

こういう差分が素早く確実に読み取れるような工夫も、テストコードには大事です(特有なわけではなく、プロダクトコードでも役に立ちますけれど)。定数を使う、インデントを揃える、コメントを入れる、名前付きパラメータやデフォルト値を活用する、フィクスチャなどで再利用する、前準備をテストのネストで構造化する、ヘルパー関数を作る、カスタムアサーションを作るなどのテクニックがあります。

テストとしてのリファクタリングも、ここで考えるべきです。テスト駆動開発で書くテストは、開発を進める順番に書いていくので、後から見ると「なにをテストしてるのか」「なんでテストしてるのか」わからなくなりがちです。ひとまとまりの機能が完成して、その機能をテストがだいたいカバーしているタイミングで、今までに書いたテストを見渡します。一貫性があるか? 仕様、なにをする機能か(What)がテストから読み取れるか? テストどうしの見分けがつくか? 全体としてどんなテストをしているか把握できるか? 他の機能のテストと並べてみて、意味があるか? テストの重複はないか? 足しておきたいテストは?

作業が一区切りついたとすると、次にこの部分をさわるのは、もしかしたら半年後かもしれません。この部分をまったく知らない別の人(ペア)が、また新しい仕様変更に対応するかもしれません。そのとき、テストが作りっぱなし、書きっぱなし、半年前に作業したペアにしかわからない状態だと、ひどく苦労することになります(そして元々のペアも半年前のことはもちろん覚えていません)。「テストが整理されていない」状態では、テストを適切に変えることもできなければ、テストを命綱として安全に変更することもできなくなります。

テストコードを他の人が読むことを想定し、またテストの内容や網羅性、観点やテスト全体での位置づけを伝えるのが大事だという認識で、テストコードをリファクタリングします。テスト名を仕様の言葉で表現し、正常系なのか異常系なのか、境界値をテストしてるのか、組み合わせをテストしているのか、見分けがつくようにします。テストをネストして分類したり、同種のテストをグルーピングしたりします。パラメタ化テストも便利です。こうしてリファクタリングされたテストは、QAテストでも役に立ちます。

テストのリファクタリングでもっとも難易度が高いのは、テストの削除です。すでにあるテストが、重複しているように見えるのだけど本当に消していいのかわからない…ここで消してしまう勇気はなかなか出ません。実際問題として、自信を持ってテストを消せるのは、テストを書いた当人だけ、テスト書いた直後だけです。書いたばかりのテストをリファクタリングするタイミングであれば、これはこういう事情で書いたけど、もういらないという判断ができます。不要なコードを後に残すのは、メンテナス性を下げるだけです。不要なテストの削除も含めて、テストのリファクタリングを忘れないでください。

コードとテストが支え合う

もうひとつ、テスト駆動開発ではプロダクトコードとテストコードを使ってお互いに動作確認をしあうことがあります。テスト駆動開発の技法として仮実装(Fake It)というものがあります。これは、テストコードを書いた段階で、確実にテストを成功させる(Greenにする)ため、プロダクトコードに偽物(Fake)の簡易な実装をします。実際の計算をする代わりに固定の値を返すなどです。もしもこの時点でテストが成功しなかったら、問題があるのは(固定値を返すだけの)プロダクトコードではなく、テストコードです。こうして、テストコードが正しく動作するかを確認します。テストのための初期化が足りていなかったり、呼び出す対象が間違っていたり、結果の確認方法が違うなどの問題を検出できます。

同じアプローチは、書き始め以外でも使います。コードのこの部分を変えると、このテストがこういうふうに失敗するはずだと考えて、実際に変えてテストを実行します。ここでテストが失敗しなかったり、失敗しても想定と違うメッセージを出していたりすると、テストが間違っている(あるいは自分の理解が違う)とわかります。

TDDのレッド・グリーン・リファクタリングのサイクルは、数分くらいとごく短いですが、このアプローチを使っているときは

  1. 試しに1文字変えて実行→
  2. 期待通りRed→
  3. 正しいつもりで数文字直して実行→
  4. 期待通りRed→
  5. さらに数文字直して実行→
  6. 期待と違うRed→
  7. いったんコードを戻してGreen→
  8. テストを直してRed→
  9. 正しく直してGreen

というように、1分以内、秒単位のサイクルで、コードとテストの挙動を細かく確認しながら進める場合もあります。こうして、テストが機能していなかったり、想定と違うテストをしてしまっているような状況を防ぎます。

こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問5. 実際にTDDで運用したとき、それでも不具合が発生したことがありました。 データの量、質が問題視されたことがありました。 受け取った情報の信憑性は会話しながら模索しないといけないのでしょうか

かつては、TDDしていればテストはそれで十分、それ以上のテストは不要と言われていたことがありました。現在は、TDDだけで品質を確保するのは難しく、それ以上のテストも必要だと言われています。

テスト駆動開発は、開発手法です。開発を駆動するために、プログラマーがフィードバックを得る道具としてテストを書きます。このテストは、開発を進めるには十分ですが、品質を担保するには不十分です。とりわけ、仕様そのものが曖昧だったり、抜け漏れがあるような場合、テスト駆動開発は無力です。

かつて「TDDで十分」と言われていたのは、これは個人的意見ですが、当時実践していたプログラマーがテスト設計もできるようなスーパーマンの人たちで、TDDのサイクルを利用していろいろな観点からテストを十分にできていたからかもしれません。いま挙げた、仕様に問題があるような場合でも、TDDをしながら「この仕様おかしいな?」と気づいて、お客様に問い合わせできていたかもしれません。(TDDには仕様の不備を見つけやすい効果があると、個人的には感じていますが、誰でもそれだけで十分というわけにはいきません。)

そういうわけで、ある意味至極当然ですが、TDDによる開発とは別にテストをすることになります。この「テストをする」は、手動テストと、テストを追加で自動化する両方を含みます。仕様書をレビューしてテスト設計をする活動が開発と並行して進めたり、開発できたものを触りながらテストしたり、開発完了前に自動化したテストを準備したり、チームやプロジェクトのテスト計画をもとした活動になります。

どんなテストが足りないか

まずTDDで書くのはユニットテストコンポーネントテストに当たるのがほとんどです。それ以外のテスト、統合テストやシステムテストなどは、他のアプローチが必要です。またユニットテストでも、TDDだけではテスト観点が足りていないので、あらためて追加のユニットテストをする必要があります。TDDしているなら、追加も自動化するのが自然でしょう。

TDD以外でテストを自動化することを、対比してQAテストと呼ぶこともあります(もっと広い活動を指してQAテストと呼ぶほうが一般的だと思います。ここでは説明のため、TDD後のテスト自動化をQAテストと呼びます)。TDDでは開発を駆動するためのテストを書きましたが、QAテストでは品質を保証するためのテストを書きます。この過程ではTDDで書いたテストも利用されますし、また整理や再構成(リファクタリング)の対象にもなります。QA観点ではテストが足りない場合もあれば、過剰なこともあり、またテストの位置づけを変えたほうが理解しやすかったりもします。

こちらテストを書くか書かないかの判断の話 · GitHubではこのように述べられています。この方は「TDDのサイクルを利用していろいろな観点からテストを十分にでき」そうに思えます。

ある程度実装が固まり、これでいこうと内心思えたタイミングがテストケースの網羅率を上げるタイミングです。ここでは TDD のテストではなく、QA (品質保証) としてのテストの観点に自分の頭を切り替えます。ここで初めて、同値分割や境界値分析による単機能のテスト設計をします (とくに無効同値クラスに分類される異常系の処理のためのテストを充実させることが多いです)。

こちらTDDのテスト vs. QAのための単体テスト について考えてみた - What is it, naokirin?では、TDDで書くテストと、QAのための単体テストの違いについて考察されています。

TDDのテスト 単体テスト
コーディングのため 品質(保証)のため
開発促進のため 欠陥を取り除くため
リファクタリングのため 品質確認のため
テストファーストで行う コーディング後に行う
詳細設計によるテスト 単体テスト設計によるテスト
設計技法としてのテスト 検証と妥当性確認のためのテスト
開発者が即座にフィードバックを得るため
プログラマのためのテスト
三角測量

こちらJaSST'12 TokyoのTDDセッションとWACATEセッションに登壇 - 千里霧中では、goyokiさんが2012年のJaSST(ソフトウェアテストシンポジウム)でのVOTDD(検証指向TDD)で、TDDのサイクルに検証指向(品質保証のひとつの観点)を含めるやり方について、発表とデモをされています。発表スライドが表示されない場合があるようですが、ページ内のリンクからアクセスできます。

資料の最後の方に「VOTDDの定義」の解説ページを追加しています。詳細はそこを参照して頂くとして、非常に簡略化して言うと、TDDでもテストについてもっと気をつければ色々良いことあるよというのがVOTDDです。

TDDでどのくらいテストを書くか、QAテストをどのくらい追加したりリファクタリングしたりするかは、プロダクトや組織によっていろいろです。気になるところをちょっと追加する程度から、何倍ものテストを書き足すところもあります。これは品質保証の戦略と、その中でのテスト自動化の位置づけに関わってくる、大きなテーマの一部でもあり、私が紹介できる範囲を超えています。

自分ならどうするか(TDDと直接関係ない補足)

元の質問に戻って、データというのはシステムへの入力データで、テストで使ったデータが不十分だったり、検討不足だったという状況かなと想像しました。個人的な経験も踏まえて考えてみます。お客様から「これが入力データだから」とデータフォーマットやテストデータを受け取った場合は、

  • 本物(本番データ)と、必ずどこか違う
  • 「0か1だけ」とされているフィールドに2が入るようなことが、最低1箇所はある
  • 本番で想定する件数(データ量)を扱うと、なにか問題が起きる
  • あり得ない矛盾したデータが本番では来る(存在しないデータの更新差分とか、明細が0件のオーダーとか)
  • 「これが最終版です」というバージョンが来てから、また新しいバージョンが届く

こういうことがあると想定します。想定した上で、開発と並行してどんなテストケースが必要か考えつつ、自分たちで使うテストデータを作ります。ここで作ったテストデータは、場合によっては、テスト駆動開発のサイクルで使うデータにすることもあります。あとで手動で流す用にすることもあります。本番環境の実データと比較して、どこが想定と違ったか調べる材料にも使います。

ソフトウェアの性質にもよりますが、別システムから受け取るデータは「得体の知れないよそもの」と思って、実行時のバリデーション(入力チェック、フォーマットチェック、整合性チェック)もかけます(これはfail fastのアプローチでもあります)。お客様のことは信頼しても、お客様が運用しているシステムは信用しないというわけです。バリデーション処理の開発は、いろいろ想像力を膨らませながら異常なパターンを考えて設計します。TDDのサイクルで、新たな可能性に気づいてパターンを補充したりもします。

現実として不具合が発生してしまったら、それ自体は仕方がないので対応します。可能な限り、不具合を再現するテスト(データも含む)を必ず書きます(TDDもそうでなくても)。テストを書いた上で修正をするという順番です。 もうひとつ、不具合がどうして発生してしまったのか分析するのも大事です。テスト漏れだとなったら、既存のテストも見直して漏れがあったら追加して自動化します。ここで新たに(未発生の)エラーを見つけられるかもしれません。コミュニケーションの改善が必要であれば、そうした施策を打ちます。QAチームがいたら分析と対策の活動を主導してくれるはずです。

こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問3. かなり最初の方のスライドにありましたが、「テスト自動化で新規のバグが見つかることは稀である」の意図とは何でしょうか?

STARソフトウェアテスト自動化研究会が提供している、テスト自動化の8原則ですね。

sites.google.com

  1. 手動テストはなくならない
  2. 手動でおこなって効果のないテストを自動化しても無駄である
  3. 自動テストは書いたことしかテストしない
  4. テスト自動化の効用はコスト削減だけではない
  5. 自動テストシステムの開発は継続的におこなうものである
  6. 自動化検討はプロジェクト初期から
  7. 自動テストで新種のバグが見つかることは稀である ←コレ
  8. テスト結果分析という新たなタスクが生まれる

これらの原則は、どのようなドメイン、プロセス、ツールの現場におけるテスト自動化であっても共通して言える、テスト自動化に取り組む前に留意しておくべきことがら=原則を、テスト自動化研究会のメンバーによる議論のうえ、絞り込んだものです。これからテスト自動化に取り組まれる方、現在取り組まれている方、これから見直しをされたい方にご参考いただければ幸いです。

簡単に言ってしまえば、「いったん正しく動くようになったものは、基本的にはそのまま正しく動き続ける」ので、その動きをなぞるテストを自動化しても、未発見のバグを発見する装置にはならないという意味です。 同じページから、こちらの7番の解説も引用します。

7. 自動テストで新種のバグが見つかることは稀である

運用に乗った自動テストは基本的に「枯れた」テストケースを対象とするため、ほとんどの種類のバグはテストケースを枯らす過程、あるいは自動テストを実装する過程で既に人間によって発見されているはずである。多くの運用に乗った自動テストの意義は「一度動いたはずの機能がうっかり壊れる」ことを最速で発見することにある。ただし、手順が同じでデータの種類が膨大なテストを自動化する場合、ファジング、テストパターンを有機的に生成できるAPIレイヤのテスト、ブラウザやRDBなどのバージョンアップの影響を受けていないことを確認するテストなど、いくつかの例外もある。

以前に直したバグが何らかの理由で再発してしまう、という事態を防ぐ役には立ちます。人間は同じミスをやらかしがち、と考えると、それほど悪くないようにも思えます。

逆に言うと、未発見のバグ、新種のバグを発見するには人間が手作業で、機械的ではない創造的な操作、観察、分析、推論などをしながらおこなう、探索的テストが向きます。探索的テストの結果から、発見したバグを再現するテストを自動化するのが効果的です。

質問4. 人によって「よいコード」=リファクタリングの着地点が違うのでは…?

その通りなので、着地点はチームとして合意しておきます。

質問2.で紹介した、B.「リファクタリングする」という動詞では、このコードをどういうふうにきれいにすべきか、探索しながら決めていきます。ペアプログラミングをすれば、ペアで相談しながら落とし所を決められます。 いずれにしても、よいコードは個人にとってではなく、チームにとってよいコードでなくては、意味がありません。

書籍『リーダブルコード』では「コードは他の人が最短時間で理解できるように書かなければならない」という、客観的指針を提示しています。Point of Useに向け、ソフトウェアを直しやすくしたいと考えたとき、実はプログラマーはコードを書くより読むことに時間をかけており、一説には10倍以上だとも言われています1。読む時間を短縮できるようにリファクタリングするというのは、有用な選択肢です。

個人の好き嫌い、センス、バックグラウンドなどによって、「よいコード」「きれいなコード」の基準は変わってきてしまいます。誰かの基準を押しつけるべきではないですが、チーム全体にとって読みやすくするには、個々人の状況も加味する必要があります。また言語、フレームワークアルゴリズムに習熟すると、「読みやすい」基準も変わってきます。たとえば「1からnまでの整数を合計する」ような簡単な処理でも、いくつか書き方があります。どれが最短時間で読めるかは、チームのスキルで変わってきますし、成長によっても変化します。

Pythonの例:

# a)  
s = 0
for i in range(n):
  s += i + 1

# b)
s = sum([i + 1 for i in range(n)])

# c)
s = n * (n + 1) / 2

「最短時間で読めるコード」は、人によって違い、チームによって違う。同じチームでも時期によって違う。そう認識すると、いわゆる標準化でカバーできる領域はそれほど大きくありません。経験と知識共有と成長の過程で、合意しつつ、少しずつアップデートしていくものとなります。

www.amazon.co.jp

こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問1. テスト駆動開発した結果のテストコードは、最終的にテスト自動化に寄与しますか?

はい、テストコード自体は寄与しないことも多いですが、テスト駆動開発自体はテスト自動化に寄与します。

テスト自動化にあとから取り組むとき、最大のネックは「テストが書けない」ことです。これはテストを差し込んだり、テスト対象を独立して取り出したりするのが、依存関係のせいでできない状態です。典型的には、1つの処理が何百行にも渡っていて、一部だけをテストできない。1クラスのロジックをテストしたいのに、他のクラスやデータベース、ファイルやシステム間通信まで必要になって、対象だけを取り出せない。

これらは設計の問題ですが、テストを書こうと思わなければ特に問題にならない場合もあります。(もっと言うと、変更しようと思わなければ問題はない。) そのため、後になってからテストを書こうとすると、設計変更が必要になってしまい、よけいに手間がかかります。

テスト駆動開発を導入していると、個々のクラス、個々のロジックについてテストを書いているので、自動的にテストが書ける設計になります。こうなっていると、テストを書き足すのは簡単です。また、設計を評価する一般的な指標である、高凝集・低結合や、単一責任原則、抽象への依存などにも近づきやすくなります(これは自動的にというよりは、意識しやすくなるという感じです)。そういう意味で、テスト自動化をやりやすい状態が実現できます。

上記の議論は、ほぼユニットテスト(コンポーネントテスト)の話になります。テスト自動化はそれ以外の、統合テストやシステムテストも対象になりますが、こうしたテストについては、テスト駆動開発はあまり関係ないと思います。BDDなど、近いけれど別のアプローチが有用かもしれません。

テスト駆動開発で書いたテストコード自体は、テストの整理(テストのリファクタリング)で整理統合されたり、品質観点のテストを書く中で吸収されてしまったりすることも多く、テスト駆動開発でテストをたくさん書いたから自動化は早く終わる、というふうにはいかないと思います。

質問2. リファクタリングはかなりの高技術だと思います。今回はそこには触れないのでしょうか?

リファクタリングについて、今回の講演では主にその効果の話だけをして、具体的なやり方には触れませんでした。それは実際のコードに触れながらでないと、理解が難しいと思います。和田卓人さんによるテスト駆動開発の講演とライブコーディングの動画が、リファクタリングの実際についても基本がわかるので、おすすめです。

 

youtu.be

ここでリファクタリングについて、もう少し詳細な話を補足します。リファクタリングという言葉は、3つくらいの側面があります。

  1. 技法としてのリファクタリング、「リファクタリング」という名詞 ― コードの動作(機能)を変えずに構造を変えて、読みやすくしたり、変更しやすくする一連のテクニック。ほんの数行を変える程度の、小さな規模のものが主。1つ1つはシンプルだが、種類が多い。
  2. プロセスとしてのリファクタリング、「リファクタリングする」という動詞 ― 一連のリファクタリング(A)を適用して、ソフトウェアの機能を変えずに維持したまま、再構築すること。数時間以上かかることもある。テスト駆動開発のサイクルでは毎回「リファクタリングして」ソフトウェアを直し続ける。
  3. 取り組みとしてのリファクタリング ― 問題意識を持って、ソフトウェアの一部についてきれいに作り直すべく、仕事として取り組む活動。数日から数ヶ月かかることもある。

Aは技法としては簡単ですし、ツール(IDE)が自動実行できるものもあり、誰でもできるようになります。ただしAのリファクタリングには「方向性」がありません。X→Yというリファクタリングも、X→Zというリファクタリングも、Y→Xというリファクタリングもあります。XとYとZのどちらがよりきれいなのかは、リファクタリングを越えた設計論が必要になります。どう命名すれば読みやすくなるか? 長くても読みやすい方がいいのか、短い方がいいのか? 似た処理を共通化した方がいいのか、意味が違うからと別にしておいたほうがいいのか? このメソッドはpublicかprivateか?

Bのリファクタリングは、設計論を知った上で「こういうふうにしたらきれいだろう」という方向性を持ち、そちらに向けてAのリファクタリングを積み重ねる作業になります。そのためには設計論を知っていたり、チームとして合意している必要もあります。

Cは大規模な設計改善です。ひとつのプロジェクトと考えられます。計画もあるし、改善後の設計イメージをあるていど固めてから実施します。

このように考えていくと、リファクタリングというのは確かに高度な技術です。これはしかし、リファクタリングそのものが高度なのではなく、作っているソフトウェアをきれいに直すために高度な技術が必要だと考えることもできます。リファクタリングは、その技術をちゃんと適用していく時間だと言えます。

www.amazon.co.jp

「テスト自動化とテスト駆動開発」講演資料

クライアント企業から依頼をいただいて、「テスト自動化とテスト駆動開発」という講演をしました。その資料を公開してよいことになったので、(若干手を入れて)公開しています。

 

speakerdeck.com

以下のような内容です。 

ねらい

  • 主に顧客向けの業務システム(B2B)を開発している、
  • プロジェクトベース、ウォーターフォールプロセスが主流の開発現場や運用保守の現場にいる、
  • マネージャーのかたに向け、
  • テスト自動化が自分たちのメリットになると納得してもらい、
  • その道筋として2つのアプローチを紹介して、
  • 組織的・長期的に取り組む価値を感じてもらう

アジェンダ

  1. 自動化したい理由
  2. 必要な人材を考える
  3. テスト自動化の端緒 ~テスト駆動開発について~
  4. 深めつつ広げる鍵 ~ペアプログラミングについて~
  5. 見る夢について

質問と回答

また、講演の際に質問をたくさんいただきました。回答をテキストで書いたものを、こちらも公開しています(いきます)。質問11件に対して、Wordで書いていたら21ページ分になったので分割しています。

最初のツイートが400いいね超という、ちょっとびっくりするくらい見てもらえているようです。

 

ここから下は、資料公開時の補足・言い訳です。

講演全体の構成として以下のような話の流れです。現在テストの自動化ができていない、稼働中システムのカバレッジという意味でも、自動化できるスキルが足らないという意味でも、できていないという状況にある。テスト自動化を推進しようと思ったとき、テスト駆動開発に取り組んで少しずつテストを書きながら、メンバーの学習と実践の場を作っていく、という作戦を提案しています。

別の作戦としてはSETの軍団に一気に自動化してもらう(雇用は難しいと思うんだけど発注はできそう)とか、自分たちでやるにしてもE2Eテストの自動化を中心にするという方法も、あると思います。そもそもアーキテクチャから変えればいい、みたいな話もあり得ます。このときはテスト駆動開発は関係なさそうです。

今回テスト駆動開発作戦を選んで提案した理由は、以下のようになるかなと考えています。

  • 中の人が、テスト駆動開発を推進したいというパッションを持っている。組織で推進する上ではとっても重要なファクター
  • 新技術・動向へのキャッチアップの力は組織全体で見たとき強くなく、安定とか効率化への力が強い。雇用も比較的安定している
  • 長期的に組織を強くしたいという、発注者の方の思い
  • テスト駆動開発の話ならできる。他の作戦なら、他の人が話すべき

逆に、そういう状況・前提・背景を取っ払った上でテスト自動化とかテスト駆動開発とかペアプログラミングとかを考えたら「今どきはこうだろー!」とか「こっちのほうが最強!」とか「やっとむてめーわかってないな!」とか、そういうツッコミがたくさんあるんじゃないかとも、思います。たとえば、モブプロじゃなくペアプロを取り上げたのは中の人の希望をいれた結果だったりします(モブプロいいよね。ペアプロもいいんだけど)。

そういうツッコミは歓迎ですし(あんまり痛くしないでくださいね)、意見や議論が出てきて盛り上がったりしたら、また嬉しいなあとも思います。

 

Dave Thomasが"Agile Is Dead (Long Live Agility)"を書いたきっかけ

あやふやな記憶からFacebookのみなさんに助けていただいたので、記録として。

pragdave.me

あらすじ(動画の話をベースに):
インドで開催されるカンファレンスへ飛行機で移動していたときのこと。時間の関係で、登壇者がたくさん乗っていた。自分の前の席にもアジャイルコンサルタントかトレーナーが2人いて、しゃべってるのが聞こえてきた。聞いていると、話題はプログラミングの話へ。
「プログラム書いてないなー」
「もう10年書いてないよ」
「こっちは12年だよ」
 
プログラムを書くことを教える立場の人が、プログラムを書いてない? それってもう書けないくせに教えてるってことだろ!? とすごく頭にきて、帰ってから書いたのが "Agile Is Dead (Long Live Agility)" だったんだ。

 

情報源
InfoQの記事: 
本人がその話をしている動画(24:20くらい):
角さんによる紹介(9枚目): 
 

www.slideshare.net

Facebookのスレ: 

www.facebook.com

asobannのAWS ECSデプロイでやったことと引っかかったこと その3

yattom.hatenablog.com

その1でasobannをAWS ECS上に構築その2でその構成をCloudFormation化した。したのだけどもう一息、Service Discoveryの対応と、MongoDBのデータ永続化が残っている。

SRVレコード対応にはPythonコードの実装が必要だった

その1にも書いたが、起動タイプがEC2でネットワークモードをbridgeにすると、個々のタスクはIPアドレスとポート番号の組み合わせでアクセスすることになる。1個のEC2インスタンスが10.0.0.82にあって、その上にアプリのタスクが3つ、それぞれポート32801, 32802, 32803で動いていたりする。公開ポートはDockerのポートフォワーディングによって、基本ランダムに決まる。ロードバランサーからアプリを指定するのはALBのTarget Groupを使うと、自動でマッピングしてくれる。

いっぽうアプリからMongoDBやRedisを指定するのは、Service Discoveryを使う。Service DiscoveryはプライベートのDNS名前解決なのだけど、DNSでIPとポート番号を両方解決するためにSRVレコードを使う。

このSRVレコードというのは初耳だったが、MongoDBは接続文字列でサポートしていた。mongodb+srv://... と書けばよいのだけど、気をつける点(引っかかった点)がいくつか。

  • 今回のアプリはPythonで、FlaskからPyMongoを使っている。SRVを使う接続文字列をPyMongoで扱うには、DnsPythonが必要で、パッケージをインストールしないといけない
  • サービスのURIのホスト名がhost.domain.tldという具合に3つ分ないと、エラーになる。たとえばmongodb.asobann-dev ではダメで、mongodb.asobann-dev.local ならいける。PyMongoがmongo+srv://に対応するときに利用するDnsPythonの制約。Private Namespaceを作るときに気をつける。
  • mongo+srv://を指定すると自動でtls=trueになるので、明示的にtls=falseを接続URIに付加する必要がある。とドキュメントに書いてあるが嘘で、ssl=falseじゃないとエラーになる。これはPyMongoの実装の問題な気がする。
  • SRVレコードは_Service._Proto.Nameという名前でレコードを作ることになっている。接続文字列が mongodb+srv://server.example.com/ だったら、AWS::ServiceDiscovery::ServiceのNameで指定するのは_mongodb._tcp.server.example.com.になる、という感じ。

Redisはそういう自動でSRVを解決するみたいな機能がまだないみたいなので、自前で何とかしないといけない。さっきDnsPythonを入れたので、以下のようなコードを書いて自分で解決してRedisに接続すればいい。(PyMongo内部の実装 を参考にした。なお本来のSRVの仕様では複数のAレコードに対応したり、TXTレコードから追加情報を引けたりするのだが、今回は端折っている。あとRedisはSSLもサポートしているが、それも省略。)

 def resolve_redis_srv(uri: str):
     '''
     Resolve redis+srv:// and return regular redis:// uri.
     Redis itself does not support connecting with SRV record but current AWS ECS
     configuration requires to use SRV record.
     Does not support TXT record.
     :param uri: connection uri starts with redis+srv://
     :return: redis://host:port/ uri resolved with SRV record
     '''
     assert uri.startswith('redis+srv://')
     import re
     from dns import resolver
     auth, host, path_and_rest = re.match(r'redis\+srv://([^@]*@)?([^/?]*)([/?].*)?', uri).groups()
     results = resolver.resolve('_redis._tcp.' + host, 'SRV')
     node_host = results[0].target.to_text(omit_final_dot=True)
     node_port = results[0].port
     node_uri = f'redis://{auth or ""}{node_host}:{node_port}{path_and_rest or ""}'
     return node_uri

EBSをDockerボリュームマウントするためREX-Rayドライバを使う

MongoDBはAWS DocumentDBを使えばマネージドサービスで楽ちんと思っていたら、課金がインスタンス時間単位で最小構成のdb.t3.mediumでも月額57USDとかになる。これにIO数やデータ量への課金もあるので、今のasobannで使うには贅沢すぎる。

というわけで、MongoDBも普通にServiceとしてECSで動かすことにした。なにもしないと、タスク再起動やEC2インスタンスの作り直しの時点でデータが消えてしまう。そこで、MongoDBのデータのみを別に保存するようにしたい。Dockerボリュームのマウントは、比較的最近(といっても2018年)にサポートされたらしい。EC2起動タイプではREX-Rayドライバを使えばEBSに保存すればいいようだ。

基本的な考え方から手順の解説まで見つけ、ほぼ同内容だけどYAMLで書いてあるものもあった。これに従ってTask Definitionを書き、スタックを更新したらすんなり成功して、よしこれでEC2インスタンスを停止してもデータが残るぞ!と思ったら残らなかった。コンソールから見てもEBSを使ってる様子がない。

結論としては、Task DefinitionのUserDataにあるrexrayドライバー(プラグイン)インストールのスクリプトが動いていなかった。この部分なんだけど、最終的にはdocker plugin ... の行だけにしたら動くようになった。

 #open file descriptor for stderr
 exec 2>>/var/log/ecs/ecs-agent-install.log
 set -x
 #verify that the agent is running
 until curl -s http://localhost:51678/v1/metadata
 do
    sleep 1
 done
 #install the Docker volume plugin
 docker plugin install rexray/ebs REXRAY_PREEMPT=true EBS_REGION=<AWS_REGION> --grant-all-permissions
 #restart the ECS agent
 stop ecs 
 start ecs

このスクリプトの結果は、/var/log/ecs/ecs-agent-install.logでわかる。curlでEC2インスタンス上のECSエージェントの起動を確認しているようなのだが、そんなものは起動していない。stop ecsだのstart ecsだのあるけど、そんなコマンドはない(systemctlのことだろうか?)。Metadataのcommandの02_start_ecs_agentという記述も、同じく動いていない。

   # 関係ない箇所を削除している
   ContainerInstances:
     Type: AWS::AutoScaling::LaunchConfiguration
     Metadata:
       AWS::CloudFormation::Init:
           commands:
             02_start_ecs_agent:
               command: start ecs

最終的にTask Definitionはこう、LaunchConfigurationはこんな設定となって、無事にデータを永続化できるようになった。関係ないけど、execとsetでこんなふうにログが出せるというのは知らなかったので勉強になりました。

ひとまずできた

以上で、asobann開発環境としての扱いで、AWS ECS上に構築できるようになった。この時点のCloudFormationテンプレートはこちらです。実行方法もREADMEに書いてあります。

github.com

yattom.hatenablog.com

yattom.hatenablog.com