Goto Fail、Heartbleed、そしてユニットテスト文化
2014年初頭に、Appleの「goto fail」バグとOpenSSLの「Heartbleed」バグという、2つのコンピュータセキュリティ上の欠陥が発見されました。どちらも、広範囲にわたる深刻なセキュリティ障害を引き起こす可能性があり、その全容は決してわからないかもしれません。これらの深刻さを考えると、ソフトウェア開発の専門家は、将来このような欠陥を防ぐ能力を向上させるために、どのようにして検出できたかを振り返ることが重要です。この記事では、ユニットテストが果たすことができる役割について検討し、ユニットテスト、そしてより重要なことに、ユニットテスト文化がどのようにこれらの特定のバグを特定できたかを示します。さらに、そのような文化のコストとメリットを見て、そのような文化がGoogleでどのように浸透したかを説明します。
2014年6月3日
2014年の初めに、インターネットのセキュリティは2つの深刻な欠陥によって揺さぶられました。Appleの「goto fail」バグ(CVE-2014-1266)とOpenSSLの「Heartbleed」バグ(CVE-2014-0160)です。どちらも、インターネット上の安全な通信の大部分が依存しているSecure Sockets Layerテクノロジーの脆弱性でした。これらのバグは、壊滅的であったのと同様に、教訓的でもありました。それらは、あらゆる規模および分野のプロジェクトを襲う、プログラマーの楽観主義、過信、そして性急さに根ざしていました。
私はユニットテストのメリットを見て、経験してきたので、これらのバグは私の情熱をかき立てます。そして、この強く刻まれた経験が、ユニットテストのアプローチが、これらのSSLバグのような影響が大きく、注目度の高い欠陥をどのように防ぐことができたかを振り返るよう私に駆り立てます。ユニットテストとは、自動化されたユニットテストを適用するのに便利な「ユニット」となるコードの塊を探すプロセスであり、低レベルの実装の詳細を検証し、コーディングエラーを早期に検出するように設計された小さなプログラムです。欠陥の性質に触発され、私はエラーを再現し、修正を検証するために、独自の概念実証のユニットテストを作成しました。私は自分の直感を検証し、これらの欠陥を早期に、そして英雄的な努力なしにユニットテストがどのように検出できたかを他の人に示すためにこれらのテストを書きました。
ユニットテストの作成は、低レベルのコーディングエラーを検出する以上のメリットを生み出します。この記事では、ユニットテストが「goto fail」バグとHeartbleedバグの防止に役立ったかどうかという疑問を探ります。そうすることで、セルフテストコードの経験が普遍的なものになるように、日常の開発の一部としてのユニットテストの採用を強く促すケースを確立したいと考えています。私は、事後分析またはプロジェクトの回顧の精神で、将来の同様の失敗を回避するのに役立つことを願って、私の洞察を提供します。私の経験は、私が私の権威に基づいて敬意を払われるべきだという意味ではありませんが、より多くの人々や組織がユニットテスト文化のメリットを検討するように促すのに十分な説得力のある事例を作成したいと考えています。
多くの人気のある技術メディアの記事は、これらの欠陥がどのように発生したか、なぜそれらが広く展開される前に既存の安全対策をすり抜けたのか、そしてそのようなバグが再び発生するのを防ぐために何をすべきかの説明を掲載しています。これらの分析のほとんどが、的外れな安易な言い訳に頼っており、最新のソフトウェアシステムの複雑さが絶えず増大しているために、そのような欠陥をあきらめて受け入れることを助長していることに私は困惑しています。まるでソフトウェア業界全体が、そしてそれに依存する一般大衆も、そのような失敗を不可避の運命、テクノロジーが私たちに与える現代の利便性のために支払う代償として受け入れることに熱心であるかのようです。悪い状況を理解し、社会として先に進むことを可能にする、最も簡単な説明なのです。
私はそのような欠陥を不可避とは受け入れません。むしろ、私たち開発者が、セキュリティ上の脆弱性や、低レベルのコーディングエラーによって引き起こされるその他の影響の大きい欠陥を防ぐために、運命や、より多くの資金、またはその他の外部要因に頼るよりもはるかにうまく行う方法を振り返る機会を捉えなければなりません。バグは発生しますが、ソフトウェア開発者も一般大衆も、これほどまでに大規模な欠陥に対する応答としてそれに満足すべきではありません。深く、真摯な反省は困難であり、開発者が自分たちの人間的な限界に対する責任を受け入れることを要求するため、多くの抵抗に遭遇します。これはしばしばプログラマーの自己像に対する挑戦となります。それゆえ、特にこれらの2つのバグを深く掘り下げ、真の解決策を探し、危険な前例を確立することを避けることが非常に重要になります。「goto fail」とHeartbleedの余波で短期的にすべてがうまくいった場合、なぜ現在のソフトウェア開発プラクティスについて何かを変える必要があるのでしょうか?
goto fail
「goto fail」バグは、2012年9月にiPhone、iPad、AppleTVに初めて出荷され、iOS 7.0とOS X Mavericksに登場し、2014年2月まで修正されませんでした。導入から17か月後です。SSL/TLSハンドシェイクアルゴリズムの最終ステップをスキップするショートカットにより、ユーザーは中間者攻撃に対して脆弱なままになりました。この攻撃では、影響を受けるシステムと別のシステムの間でトラフィックを中継する悪意のあるシステムが、虚偽の資格情報を使用して安全な接続の錯覚を提示し、その後、他の2つのシステム間のすべての通信を傍受する可能性があります。
バグは、この悪名高いコードスニペットからその名を得ました
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail;
エドガー・ダイクストラの有名なエッセイGO TOステートメントに対する事例に基づいて、すべてのgotoステートメントは悪いと主張する人もいます。これは、一般的な公理である「Gotoは有害と考えられる」によって要約されています。ただし、goto fail
ステートメントは、Cプログラマーにはなじみのあるイディオムを表現しています。回復不能なエラーの場合、このようなステートメントは、ローカルに割り当てられたリソースが適切に解放される、関数の最後にある回復ブロックに制御を即座に渡します。他の言語には、ダイクストラがエッセイの結論で「アボーション句」と呼んだもの、つまりC++のデストラクタ、Javaのtry/catch/finally
、Goのdefer()/panic()/recover()
、Pythonのtry/except/finally
とwith
ステートメントのような、このような機能を組み込みでサポートしています。Cでは、このコンテキストでgoto
を使用することに本質的な問題や混乱はありません。言い換えれば、goto
はここでは有害とみなされるべきではありません。
Cプログラマーは、最初のgoto fail
ステートメントが先行するif
ステートメントの結果にバインドされているのに対し、2番目のgoto fail
はバインドされていないこともすぐに認識するでしょう。2つのステートメントのインデントが一致していても、Cでは意味がなく、複数のステートメントをif
条件にバインドするには、周囲の波括弧が必要です。最初のgoto fail
が実行されない場合、2番目のgoto fail
が確実に実行されます。これは、ハンドシェイクアルゴリズムの後続のステップが実行されることは決してないが、最終的な検証ステップが失敗したとしても、このポイントを正常に通過した交換は常に成功した戻り値を生成することを意味します。より平易に言うと、アルゴリズムは余分なgoto fail
ステートメントによってショートカットされます。
一部の人は、すべてのif
ステートメントに波括弧の使用を必須にするコーディングスタイルや、到達不能コードのコンパイラ警告を有効にすることで役立った可能性があると主張しています。ただし、ユニットテストが解決するのに役立つコードには、より深い問題があります。
ユニットテストはどのように役立ったか?
「ユニット」テストを適用する「ユニット」を探す際に、バグのあるアルゴリズムを含むコードブロック全体が、条件付きロジックのクラスターとともに、そのようなユニットとして飛び出してきます(AppleのSecure Transportライブラリのバージョン55471のSSLVerifySignedServerKeyExchange()
関数より)。
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail;
このようなコードブロックは、独自の関数に抽出するとテストが容易になります。このようにコードの塊を抽出することは、ユニットテストを書く人々の習慣的なプラクティスであり、既存のコードベースの一部を一度にテスト対象にするのに役立ちます。アルゴリズムで使用されている変数とデータ型を注意深く見ると、このコードブロックがハッシュに対してハンドシェイクを実行していることが明らかになります。また、SSLHashSHA1
の型を調べることで、これが HashReference
のインスタンスであることもわかります。これは、「ジャンプテーブル」であり、Cプログラマーが仮想関数のような動作(つまり、リスコフの置換原則と実行時ポリモーフィズム)を実装できるようにする関数ポインタを含む構造です。この操作を、その意図を示す名前を持つ関数に抽出できます(余分な goto fail
は除外します)。
static OSStatus HashHandshake(const HashReference* hashRef, SSLBuffer* clientRandom, SSLBuffer* serverRandom, SSLBuffer* exchangeParams, SSLBuffer* hashOut) { SSLBuffer hashCtx; OSStatus err = 0; hashCtx.data = 0; if ((err = ReadyHash(hashRef, &hashCtx)) != 0) goto fail; if ((err = hashRef->update(&hashCtx, clientRandom)) != 0) goto fail; if ((err = hashRef->update(&hashCtx, serverRandom)) != 0) goto fail; if ((err = hashRef->update(&hashCtx, exchangeParams)) != 0) goto fail; err = hashRef->final(&hashCtx, hashOut); fail: SSLFreeBuffer(&hashCtx); return err; }
これで、以前にバグのあったアルゴリズムを構成していた一連のステートメントは、次のように置き換えることができます。
if ((err = HashHandshake(&SSLHashSHA1, &clientRandom, &serverRandom, &signedParams, &hashOut)) != 0) { goto fail; }
この関数は、単独でより理解しやすくなります。このような自己完結型の関数に直面した場合、プログラマーはコードの外部効果に焦点を当て始め、次のような質問を検討できます。
- テスト対象のコードによって満たされる契約は何ですか?
- どのような前提条件が必要で、それらはどのように適用されますか?
- どのような事後条件が保証されますか?
- どのような入力例が異なる動作を引き起こしますか?
- どのテストセットが各動作を引き起こし、各保証を検証しますか?
HashHandshake()
の場合、契約は次のように記述できます。5つのステップがあり、すべてが成功する必要があります。成功または失敗は、戻り値によって呼び出し元に伝播されます。HashReference
は、一連の呼び出しに対して正しく応答することが期待されます。HashHandshake()
によって渡されたものを超えて関数またはデータを使用するかどうかは、HashHandshake()
自体にとって不透明な実装の詳細です。
このように単純なアルゴリズムの場合、テストケースは実装をかなり「ミラーリング」します。1つの成功ケース、5つの失敗ケースです。より高レベルまたはより複雑な操作の場合、このような密接な「ミラーリング」は脆いテストになりやすく、一般的には避けるべきです。これは、モックや他のテストダブルを使用して、コードをそのコラボレーターから分離してテストする場合に特に重要です。
コードがすべきでないことをしないことをテストすることは、間違いなくさらに重要です。
テスト対象のコードの範囲に関係なく、可能な限り徹底的に失敗ケースをテストすることが重要です。コードがすべきことをテストして、それで済ませたい誘惑に駆られますが、コードがすべきでないことをしないことをテストすることは、間違いなくさらに重要です。
概念実証のユニットテスト
Cはオブジェクト指向プログラミング言語ではありませんが、このアルゴリズムの既存のコードは明らかにオブジェクト指向のデザインを示しており、コードが独自の関数に抽出されると、実際にはユニットテストが容易になります。tls_digest_test.c
の概念実証ユニットテストは、HashReference
スタブを使用して、抽出されたHashHandshake()
アルゴリズムのすべてのパスを効果的にカバーする方法を示しています。実際のテストケースは次のようになります。
static int TestHandshakeSuccess() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = SUCCESS; return ExecuteHandshake(fixture); } static int TestHandshakeInitFailure() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = INIT_FAILURE; fixture.ref.init = HashHandshakeTestFailInit; return ExecuteHandshake(fixture); } static int TestHandshakeUpdateClientFailure() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = UPDATE_CLIENT_FAILURE; fixture.client = FAIL_ON_EVALUATION(UPDATE_CLIENT_FAILURE); return ExecuteHandshake(fixture); } static int TestHandshakeUpdateServerFailure() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = UPDATE_SERVER_FAILURE; fixture.server = FAIL_ON_EVALUATION(UPDATE_SERVER_FAILURE); return ExecuteHandshake(fixture); } static int TestHandshakeUpdateParamsFailure() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = UPDATE_PARAMS_FAILURE; fixture.params = FAIL_ON_EVALUATION(UPDATE_PARAMS_FAILURE); return ExecuteHandshake(fixture); } static int TestHandshakeFinalFailure() { HashHandshakeTestFixture fixture = SetUp(__func__); fixture.expected = FINAL_FAILURE; fixture.ref.final = HashHandshakeTestFailFinal; return ExecuteHandshake(fixture); }
HashHandshakeTestFixture
には、テスト対象のコードへの入力として、また予想される結果を確認するために必要なすべての変数が保持されます。
typedef struct { HashReference ref; SSLBuffer *client; SSLBuffer *server; SSLBuffer *params; SSLBuffer *output; const char *test_case_name; enum HandshakeResult expected; } HashHandshakeTestFixture;
SetUp()
は、HashHandshakeTestFixture
のすべてのメンバーをデフォルト値に初期化します。各テストケースは、特定のテストケースに関連するメンバーのみを上書きします。
static HashHandshakeTestFixture SetUp(const char *test_case_name) { HashHandshakeTestFixture fixture; memset(&fixture, 0, sizeof(fixture)); fixture.ref = SSLHashNull; fixture.ref.update = HashHandshakeTestUpdate; fixture.test_case_name = test_case_name; return fixture; }
ExecuteHandshake()
は、HashHandshake()
関数を実行し、結果を評価します。結果が予想と異なる場合は、エラーメッセージを出力し、エラー値を返します。
/* Executes the handshake and returns zero if the result matches expected, one * otherwise. */ static int ExecuteHandshake(HashHandshakeTestFixture fixture) { const enum HandshakeResult actual = HashHandshake( &fixture.ref, fixture.client, fixture.server, fixture.params, fixture.output); if (actual != fixture.expected) { printf("%s failed: expected %s, received %s\n", fixture.test_case_name, HandshakeResultString(fixture.expected), HandshakeResultString(actual)); return 1; } return 0; }
final()
呼び出しの前に、HashHandshake()
アルゴリズムの任意の場所に重複したgoto fail
ステートメントを追加すると、テストが失敗します。
このテストは、プロジェクトですでに使用されているツールを使用して効果的なテストを作成できることを示すために、テストフレームワークなしで作成されました。標準フレームワークを参照していなくても、前の段落の説明は比較的理解しやすいはずです。適切に編成されたオブジェクトと、適切に選択された名前を持つ関数を使用した適切に編成されたテストケースは、テストが失敗した場合、通常はテストプログラムの完全な実装を掘り下げることなく、テストケースのみの情報から失敗を診断できることを意味します。テストフレームワークは、テストをより効率的に作成するのに役立ちますが、適切に編成された徹底的なユニットテストを作成するための前提条件ではありません。
この関数を実行する一連のテストを作成することは、条件ではなく具体的な例について考えるようになったため、簡単です。さらに、テストはダブルチェックとして機能します。条件付きロジックで間違いを犯し、チェーン内の1つのテストを誤って反転させるのは簡単です。しかし、テストを作成するときは、例とロジックで2回動作を記述しています。バグが通過するためには、2つの異なる表現で同じ間違いを犯す必要があります。
このアルゴリズムを最初に記述したプログラマーは、新しいコードのエラーを確認するためにプログラムを実行した可能性が高いです。ほとんどのプログラマーは、プログラムが期待どおりに動作していることを確認するために、いくつかのサンプル入力を実行します。問題は、これらの実行がしばしば一時的であり、コードが動作すると破棄されることです。自動テストは、これらの実行を永続的なダブルチェックとしてキャプチャします。
その永続的なダブルチェックは、ここで重要です。不正な2番目のgoto fail
がどのようにコードに入ったのかは正確にはわかりません。可能性の高い理由は、大規模なマージ操作の結果であるということです。ブランチをメインラインにマージすると、大きな違いが生じる可能性があります。マージがコンパイルされたとしても、エラーを引き起こす可能性があります。このようなマージの違いを検査するのは、経験豊富な開発者にとっても時間と手間がかかり、エラーが発生しやすい可能性があります。この場合、ユニットテストによって提供される自動化されたダブルチェックは、人間がマージされたコードを検査する前に、マージエラーの可能性をキャッチする可能性が高いという意味で、迅速かつ入念(しかし苦痛のない!)なコードレビューを提供します。元の作成者がコードに「goto fail」バグを導入した可能性は低いですが、テストスイートはあなた自身のミスを見つけるのに役立つだけでなく、将来的にプログラマーによって犯されたミスを明らかにするのにも役立ちます。
「goto fail」バグの場合、テスト可能な関数を探して抽出するというユニットテストの習慣には、2つ目の利点があります。
デジャブの繰り返し
別のHashReference
インスタンスを持つ同じアルゴリズムのコピーが、同じ関数内のバギーアルゴリズムのすぐ上に表示されます。合計で、このアルゴリズムは同じファイルに6回表示されます(Security-55471のsslKeyExchange.c)。
- バグが含まれていた
SSLVerifySignedServerKeyExchange()
に2回 SSLVerifySignedServerKeyExchangeTls12()
に1回SSLSignServerKeyExchange()
に2回SSLSignServerKeyExchangeTls12()
に1回
Security-55471.14のsslKeyExchange.cの更新バージョンは、SSLVerifySignedServerKeyExchange()
から重複したgoto fail
ステートメントを削除しましたが、重複したアルゴリズムは残っています。
コードの重複は、ソフトウェアエラーの可能性を高めることが知られているコードの臭いです。また、上記の関数名から、コアハンドシェイクアルゴリズムの重複以外にも重複があることが明らかです。このカットアンドペーストコードの再利用は、重複したコードがマージ中に利用可能な「コードサーフェス」を増やし、検出されないマージエラーの可能性を増大させるため、バグが大規模なマージ操作によって引き起こされた可能性があるという仮説も支持しています。
ユニットテストは、コピー/ペーストされたコードもユニットテストする必要があるため、コピー/ペーストを最小限に抑える圧力を導入します。テストが容易であるため、このアルゴリズムのコピーが1つしかないことを保証できた可能性があります。ユニットテストは、このアルゴリズムが正しいことを簡単に検証できたはずであり、マージであろうとなかろうと、「goto fail」バグが最初に書き込まれるのを防ぐことができたはずです。
また、このライブラリの多数のSSL回帰テストをリストしているように見えるSecurity-55471バージョンのssl_regressions.hは、Security-55471.14バージョンのssl_regressions.hで変更されていません。ライブラリの2つのバージョンの唯一の実質的な違いは、goto fail
ステートメント自体の削除であり、テストの追加や重複の排除はありません。
$ curl -O http://opensource.apple.com/tarballs/Security/Security-55471.tar.gz $ curl -O http://opensource.apple.com/tarballs/Security/Security-55471.14.tar.gz $ for f in Security-55471{,.14}.tar.gz; do gzip -dc $f | tar xf - ; done # Since diff on OS X doesn't have a --no-dereference option: $ find Security-55471* -type l | xargs rm $ diff -uNr Security-55471{,.14}/libsecurity_ssl diff -uNr Security-55470/libsecurity_ssl/lib/sslKeyExchange.c Security-55471.14/libsecurity_ssl/lib/sslKeyExchange.c --- Security-55471/libsecurity_ssl/lib/sslKeyExchange.c 2013-08-09 20:41:07.000000000 -0400 +++ Security-55471.14/libsecurity_ssl/lib/sslKeyExchange.c 2014-02-06 22:55:54.000000000 -0500 @@ -628,7 +628,6 @@ goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; - goto fail; if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail; diff -uNr Security-55471/libsecurity_ssl/regressions/ssl-43-ciphers.c Security-55471.14/libsecurity_ssl/regressions/ssl-43-ciphers.c --- Security-55471/libsecurity_ssl/regressions/ssl-43-ciphers.c 2013-10-11 17:56:44.000000000 -0400 +++ Security-55471.14/libsecurity_ssl/regressions/ssl-43-ciphers.c 2014-03-12 19:30:14.000000000 -0400 @@ -85,7 +85,7 @@ { OPENSSL_SERVER, 4000, 0, false}, //openssl s_server w/o client side auth { GNUTLS_SERVER, 5000, 1, false}, // gnutls-serv w/o client side auth { "www.mikestoolbox.org", 442, 2, false}, // mike's w/o client side auth -// { "tls.secg.org", 40022, 3, false}, // secg ecc server w/o client side auth - This server generate DH params we dont support. +// { "tls.secg.org", 40022, 3, false}, // secg ecc server w/o client side auth { OPENSSL_SERVER, 4010, 0, true}, //openssl s_server w/ client side auth { GNUTLS_SERVER, 5010, 1, true}, // gnutls-serv w/ client side auth
文化的意味合い
同じアルゴリズムの6つの個別のコピーが存在することは、このバグが1回限りのプログラマーエラーによるものではないことを明確に示しています。これはパターンでした。これは、重複した未テストのコードを容認する開発文化の証拠です。
私はAppleで働いたことも、Appleの開発者を知っているわけでもありません。会社全体の開発文化がどのようなものであり、このコードが代表的なものなのか、例外的なものなのかは正確にはわかりません。このコードが規範ではなく例外であるとしても、それでも容認できません。私にとって、このコーディングエラーによってプライバシーとセキュリティが侵害された可能性がある者として、この特定のエラーを「言い訳」する状況や、残りの文化がどのようなものであったかは問題ではありません。私は、このような間違いに対するより大きな説明責任を求めています。恥をかかせたり、非難したりすることではなく、説明責任とそれに続くデューデリジェンスです。それが、次の「goto fail」の発生を防ぐためのより深い戦略です。
私は、開発文化は変化する可能性があることを知っています。ユニットテストがまだ重要な部分ではない場合、このようなバグは、私たち自身の開発文化を振り返り、ユニットテストがなぜそのような重要な開発プラクティスなのかを理解する機会を与えてくれます。このドキュメントの後のセクションで、開発文化を変える経験について詳しく説明し、単一のチームから会社全体まで、他の開発文化に変化をもたらす方法に関するアドバイスを提供します。
「goto fail」の概念実証ユニットテストは、後知恵で書かれた一回限りのテストとして簡単に片付けられるかもしれません。しかし、私はむしろ、開発チームがどこでも既存のコードに今すぐ適用できる、同様に恥ずかしい(そして潜在的に壊滅的な)バグを回避するための、アクセスしやすいユニットテスト手法の例として捉えてもらいたいと考えています。ユニットテストを重視し、そのスキル向上に努める開発文化は、「goto fail」のようなプログラミングエラーがユーザーに影響を与える前に、最も高い確率でそれらを検出するテストを生み出すでしょう。
次に、Heartbleedバグを例に、ユニットテストがどのように適用できたかを検証しましょう。
Heartbleed
Heartbleedは、どこにでも存在するOpenSSLライブラリの一部として現れた、テストされていないセキュリティクリティカルなコードの、同様に心を痛める事例でした。2012年1月に、OpenSSL-1.0.1-beta1にTLSハートビートを実装するための大規模な未テストの変更の一部として導入されました。このバグにより、攻撃者は空のハンドシェイクリクエストを送信し、最大64kのデータを送信したと宣言することが可能になりました。脆弱なシステムは、宣言されたサイズを読み取りますが検証せず、リクエストバッファに隣接するメモリの最大64kの内容で応答します。このやり取りのログは記録されず、攻撃の痕跡は全く残りません。
バグを導入した変更はコードレビューされました。レビュー担当者が、変更にユニットテストを含めることを強く要求しなかったことは明らかです。このバグは、2014年4月まで発見・修正されず、1.0.1gの一部としてリリースされました。
dtls1_process_heartbeat()
(ssl/d1_both.c
)とtls1_process_heartbeat()
(ssl/t1_lib.c
)の両方に存在していました。
int dtls1_process_heartbeat(SSL *s) { unsigned char *p = &s->s3->rrec.data[0], *pl; unsigned short hbtype; unsigned int payload; unsigned int padding = 16; /* Use minimum padding */
ローカルポインタ変数*p
は、ハートビートリクエストバッファの先頭に初期化されます。最初のバイトはリクエストのタイプを識別し、hbtype
に格納されます。次の2バイトは、クライアントが提供したリクエストデータのサイズを指定し、クライアントは応答としてコピーして返送することを期待しています。このサイズはpayload
に格納されます(変数の意図に合わせるなら、payload_size
またはpayload_len
という名前の方が良かったでしょう)。それに続くのは、クライアントが提供したデータの先頭、つまり「ペイロード」で、クライアントにコピーして返送されるもので、pl
が指します(この変数はpayload
と名付けるべきでした)。
データは対応する変数に読み込まれます。
/* Read type and payload length first */ hbtype = *p++; n2s(p, payload); pl = p;
n2s()
は、(ssl/ssl_locl.h
からの)マクロで、ポインタp
の次の2バイトを読み取り、値をpayload
に格納し、p
を2バイト進めます。
hbtype
がTLS1_HB_REQUEST
の場合、影響を受けるシステムは応答バッファを割り当て、payload
バイトをそこにコピーします(s2n()
は、応答バッファにペイロード長をコピーするn2s()
のコンパニオンマクロです)。
unsigned char *buffer, *bp; int r; /* Allocate memory for the response, size is 1 byte * message type, plus 2 bytes payload length, plus * payload, plus padding */ buffer = OPENSSL_malloc(1 + 2 + payload + padding); bp = buffer; /* Enter response type, length and copy payload */ *bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload);
memcpy()
が問題なのは、長さの値payload
が、リクエストから実際に読み取られた長さと一致するかどうかが検証されていないためです。リクエストには空の文字列が含まれていた可能性がありますが、最大64キロバイトの長さが示されていた可能性があります。その結果、リクエストバッファの内容ではなく、プロセスのメモリの最大64キロバイトが応答として返されます。繰り返しますが、このイベントのログは記録されず、文字通り痕跡は残りません。
修正では、バッファサイズに関する欠落したチェックが提供されています。
/* Read type and payload length first */ if (1 + 2 + 16 > s->s3->rrec.length) return 0; /* silently discard */ hbtype = *p++; n2s(p, payload); if (1 + 2 + payload + 16 > s->s3->rrec.length) return 0; /* silently discard per RFC 6520 sec. 4 */ pl = p;
最初のチェックは、クライアントがペイロードとして空の文字列を送信した場合を対象とし、ソケットから読み取られたデータの実際のサイズが、この最小リクエストサイズと一致することを確認します。2番目のチェックは、クライアントが提供したペイロードサイズが、ペイロードデータを含むバッファのサイズを超えないことを保証します。
dtls1_process_heartbeat()
では、ペイロードサイズが応答に対して許可される最大値を超えないことを確認するチェックが追加されました。
unsigned int write_length = 1 /* heartbeat type */ + 2 /* heartbeat length */ + payload + padding; int r; if (write_length > SSL3_RT_MAX_PLAIN_LENGTH) return 0;
ユニットテストはどのように役立ったか?
「goto fail」バグの場合とは異なり、新しい関数を抽出する必要はありません。dtls1_process_heartbeat()
とtls1_process_heartbeat()
の両方は、すでにテスト対象とするための複雑なセットアップを必要としない適切なサイズのユニットです。私たちは、「goto fail」のコンテキストで前に提起されたのと同じ質問にすぐ取り組むことができます。
- テスト対象のコードによって満たされる契約は何ですか?
- どのような前提条件が必要で、それらはどのように適用されますか?
- どのような事後条件が保証されますか?
- どのような入力例が異なる動作を引き起こしますか?
- どのテストセットが各動作を引き起こし、各保証を検証しますか?
ハートビート関数が外部から提供されたデータを含むリクエストバッファを処理することを考えると、自己テストに慣れているプログラマーは、そのような入力の処理における脆弱性、特にメモリバッファの読み取りと割り当てに関する脆弱性を探すことを習慣にするでしょう。
この自然なユニットテスターの本能に加えて、ハートビートリクエストを定義するプロトコルのセクションからの抜粋を以下に示します。
payload_length: The length of the payload. [...snip...] If the payload_length of a received HeartbeatMessage is too large, the received HeartbeatMessage MUST be discarded silently.
この場合、プロトコル仕様は、私たちにとって適切なユニットテストを実質的に定義しています。payload_length
が実際に読み取られたものと一致することを検証する必要があるとは明示的には述べていませんが、payload_length
が特別な注意を払うべきであることを強く示唆しています。
概念実証のユニットテスト
heartbleed_test.c
の概念実証ユニットテストは、「goto fail」のユニットテストよりも少し複雑ですが、同様の構造に従います。以下に、dtls1_process_heartbeat()
のテストケースを示します。
static int TestDtls1NotBleeding() { HeartbleedTestFixture fixture = SetUpDtls(__func__); /* Three-byte pad at the beginning for type and payload length */ unsigned char payload_buf[] = " Not bleeding, sixteen spaces of padding" " "; const int payload_buf_len = HonestPayloadSize(payload_buf); fixture.payload = &payload_buf[0]; fixture.sent_payload_len = payload_buf_len; fixture.expected_return_value = 0; fixture.expected_payload_len = payload_buf_len; fixture.expected_return_payload = "Not bleeding, sixteen spaces of padding"; return ExecuteHeartbeat(fixture); } static int TestDtls1NotBleedingEmptyPayload() { HeartbleedTestFixture fixture = SetUpDtls(__func__); /* Three-byte pad at the beginning for type and payload length, plus a NUL * at the end */ unsigned char payload_buf[4 + kMinPaddingSize]; memset(payload_buf, ' ', sizeof(payload_buf)); payload_buf[sizeof(payload_buf) - 1] = '\0'; const int payload_buf_len = HonestPayloadSize(payload_buf); fixture.payload = &payload_buf[0]; fixture.sent_payload_len = payload_buf_len; fixture.expected_return_value = 0; fixture.expected_payload_len = payload_buf_len; fixture.expected_return_payload = ""; return ExecuteHeartbeat(fixture); } static int TestDtls1Heartbleed() { HeartbleedTestFixture fixture = SetUpDtls(__func__); /* Three-byte pad at the beginning for type and payload length */ unsigned char payload_buf[] = " HEARTBLEED "; fixture.payload = &payload_buf[0]; fixture.sent_payload_len = kMaxPrintableCharacters; fixture.expected_return_value = 0; fixture.expected_payload_len = 0; fixture.expected_return_payload = ""; return ExecuteHeartbeat(fixture); } static int TestDtls1HeartbleedEmptyPayload() { HeartbleedTestFixture fixture = SetUpDtls(__func__); /* Excluding the NUL at the end, one byte short of type + payload length + * minimum padding */ unsigned char payload_buf[kMinPaddingSize + 3]; memset(payload_buf, ' ', sizeof(payload_buf)); payload_buf[sizeof(payload_buf) - 1] = '\0'; fixture.payload = &payload_buf[0]; fixture.sent_payload_len = kMaxPrintableCharacters; fixture.expected_return_value = 0; fixture.expected_payload_len = 0; fixture.expected_return_payload = ""; return ExecuteHeartbeat(fixture); } static int TestDtls1HeartbleedExcessivePlaintextLength() { HeartbleedTestFixture fixture = SetUpDtls(__func__); /* Excluding the NUL at the end, one byte in excess of maximum allowed * heartbeat message length */ unsigned char payload_buf[SSL3_RT_MAX_PLAIN_LENGTH + 2]; memset(payload_buf, ' ', sizeof(payload_buf)); payload_buf[sizeof(payload_buf) - 1] = '\0'; fixture.payload = &payload_buf[0]; fixture.sent_payload_len = HonestPayloadSize(payload_buf); fixture.expected_return_value = 0; fixture.expected_payload_len = 0; fixture.expected_return_payload = ""; return ExecuteHeartbeat(fixture); }
HeartbleedTestFixture
、SetupDtls()
、およびExecuteHeartbeat()
の項目は、「goto fail」の概念実証ユニットテストの同様の項目に密接に対応しています。
typedef struct { SSL_CTX *ctx; SSL *s; const char* test_case_name; int (*process_heartbeat)(SSL* s); unsigned char* payload; int sent_payload_len; int expected_return_value; int return_payload_offset; int expected_payload_len; const char* expected_return_payload; } HeartbleedTestFixture; static HeartbleedTestFixture SetUp(const char* const test_case_name, const SSL_METHOD* meth) { HeartbleedTestFixture fixture; int setup_ok = 1; memset(&fixture, 0, sizeof(fixture)); fixture.test_case_name = test_case_name; fixture.ctx = SSL_CTX_new(meth); if (!fixture.ctx) { fprintf(stderr, "Failed to allocate SSL_CTX for test: %s\n", test_case_name); setup_ok = 0; goto fail; } /* snip other allocation and error handling blocks */ fail: if (!setup_ok) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } return fixture; } static HeartbleedTestFixture SetUpDtls(const char* const test_case_name) { HeartbleedTestFixture fixture = SetUp(test_case_name, DTLSv1_server_method()); fixture.process_heartbeat = dtls1_process_heartbeat; /* As per dtls1_get_record(), skipping the following from the beginning of * the returned heartbeat message: * type-1 byte; version-2 bytes; sequence number-8 bytes; length-2 bytes * * And then skipping the 1-byte type encoded by process_heartbeat for * a total of 14 bytes, at which point we can grab the length and the * payload we seek. */ fixture.return_payload_offset = 14; return fixture; } static HeartbleedTestFixture SetUpTls(const char* const test_case_name) { HeartbleedTestFixture fixture = SetUp(test_case_name, TLSv1_server_method()); fixture.process_heartbeat = tls1_process_heartbeat; fixture.s->handshake_func = DummyHandshake; /* As per do_ssl3_write(), skipping the following from the beginning of * the returned heartbeat message: * type-1 byte; version-2 bytes; length-2 bytes * * And then skipping the 1-byte type encoded by process_heartbeat for * a total of 6 bytes, at which point we can grab the length and the payload * we seek. */ fixture.return_payload_offset = 6; return fixture; } static void TearDown(HeartbleedTestFixture fixture) { ERR_print_errors_fp(stderr); SSL_free(fixture.s); SSL_CTX_free(fixture.ctx); } static int ExecuteHeartbeat(HeartbleedTestFixture fixture) { int result = 0; SSL* s = fixture.s; unsigned char *payload = fixture.payload; unsigned char sent_buf[kMaxPrintableCharacters + 1]; s->s3->rrec.data = payload; s->s3->rrec.length = strlen((const char*)payload); *payload++ = TLS1_HB_REQUEST; s2n(fixture.sent_payload_len, payload); /* Make a local copy of the request, since it gets overwritten at some * point */ memcpy((char *)sent_buf, (const char*)payload, sizeof(sent_buf)); int return_value = fixture.process_heartbeat(s); if (return_value != fixture.expected_return_value) { printf("%s failed: expected return value %d, received %d\n", fixture.test_case_name, fixture.expected_return_value, return_value); result = 1; } /* If there is any byte alignment, it will be stored in wbuf.offset. */ unsigned const char *p = &(s->s3->wbuf.buf[ fixture.return_payload_offset + s->s3->wbuf.offset]); int actual_payload_len = 0; n2s(p, actual_payload_len); if (actual_payload_len != fixture.expected_payload_len) { printf("%s failed:\n expected payload len: %d\n received: %d\n", fixture.test_case_name, fixture.expected_payload_len, actual_payload_len); PrintPayload("sent", sent_buf, strlen((const char*)sent_buf)); PrintPayload("received", p, actual_payload_len); result = 1; } else { char* actual_payload = strndup((const char*)p, actual_payload_len); if (strcmp(actual_payload, fixture.expected_return_payload) != 0) { printf("%s failed:\n expected payload: \"%s\"\n received: \"%s\"\n", fixture.test_case_name, fixture.expected_return_payload, actual_payload); result = 1; } free(actual_payload); } if (result != 0) { printf("** %s failed **\n--------\n", fixture.test_case_name); } TearDown(fixture); return result; }
tls1_process_heartbeat()
テストは、HeartbleedTestFixture
を初期化するためにSetUpTls()
を呼び出すことと、ExcessivePlaintextLength
ケースをカバーしないことを除いて、ほぼ同じです。ExecuteHeartbeat()
およびその他のテストヘルパー関数は、「goto fail」テストのものよりも少し複雑ですが、わずかな違いです。
「goto fail」テストと同様に、このテストはテストフレームワークの助けなしに書かれました。変更なしに、1.0.1-beta1から1.0.1gまでの任意のOpenSSLリリースのtest/
ディレクトリに直接コピーして実行できます。バージョン1.0.1gで実行すると、テストはパスし、出力は生成されません。他のバージョンでは、「Heartbleed」という名前のテストケースは、次のような出力で失敗します。
TestDtls1Heartbleed failed: expected payload len: 0 received: 1024 sent 26 characters "HEARTBLEED " received 1024 characters "HEARTBLEED \xde\xad\xbe\xef..." ** TestDtls1Heartbleed failed **
失敗したテストで返されるバッファの内容は、テストを実行しているマシンのメモリの内容に依存します。テストファイルの先頭でデフォルトで1024に設定されているkMaxPrintableCharacters
の値を大きくすると、さらに多くのメモリ内容が返されるのを確認できます。
分解せよ、細分化せよ
Heartbleedの例では、「goto fail」の例では対処できなかった別の問題に取り組むことができます。「goto fail」では、バグを導入した正確な変更を把握できませんでした。利用可能な証拠は、コードの重複によって複雑化した、大規模なマージ操作である可能性を示唆しています。それでも、「複雑なマージ」理論は推測に過ぎません。Heartbleedでは、TLSハートビート機能とそれに埋め込まれたHeartbleedバグの両方を導入した正確な変更を確認でき、それがコードレビュー済みであることが分かります。
ユニットテストに慣れている開発者は、問題の単一のモノリシックな変更ではなく、機能に向けて構築された一連の十分にテストされた小さな変更を生成または要求したでしょう。上記関数のみを含む、より小さく、十分にテストされた変更であれば、作成者、レビュー担当者、または関心のある傍観者が、外部から提供された値を使用してメモリブロックを読み取ること、およびそのような値が適切に処理されたことを検証することに、より集中できたでしょう。ハートビートリクエストの構造と処理を定義するプロトコルの特定のセクションへの明示的な参照も、テストとレビューに焦点を当てるのに役立ったかもしれません。
コーディング標準ドキュメントもこのプロセスに役立ちます。名前付け、空白、中括弧の配置の詳細を指定することに加えて、このような標準では、リクエスト処理コードおよびバッファ処理コードに、バッファオーバーランの問題がないことを検証するテストを添付することを義務付けることができます。これは、レビューのために提出されたすべてのコードが、ポリシーとして新規または既存のユニットテストによってカバーされることを義務付けることに加えて実施されます。
テストされていないものは、修正されていない
上記の概念実証テストは、誰かがコードをユニットテストしようとした場合、歴史上最も壊滅的なコンピュータバグの1つをキャッチして防止できた可能性があることを示しています。概念実証ユニットテストの存在は、それが不可能だったという主張を否定します。残念ながら、バグのために提出された修正にも、バグを検証し、回帰を防ぐためのユニットテストがありませんでした。
自動化された回帰テストなしに、バグが適切に修正されたとは見なされません。
ユニットテスト文化では、バグが発見された場合、自然な反応は、それを露呈するテストを作成し、次にそれを解消するためにコードを修正することです。「goto fail」の議論で述べた点を拡張すると、コードの変更を検証するために実行される手動テストは一時的なものであり、テストを伴わない修正は元に戻ってしまう可能性があります。自動化された回帰テストは、最初のコードに対して書かれたテストができたはずのように、将来のエラーを防ぎます。
最新のバージョン管理システムの能力と、フォーク、マージ、チェリーピックの一般的になりつつある慣行を考えると、特に既知の壊滅的なバグの回帰につながる変更など、意図しない変更を防ぐために、テストはこれまで以上に重要になっています。チェリーピックまたはマージ中に回帰テストが明らかに削除された場合は、特に修正と同じ変更にテストが含まれていた場合は、修正も元に戻ってしまう可能性があるため、警鐘を鳴らす必要があります。
デジャブの繰り返し
最後に、もう1つ指摘しておきたいことがあります。dtls1_process_heartbeat()
(ssl/d1_both.c
)とtls1_process_heartbeat()
(ssl/t1_lib.c
)を個別のブラウザタブで開いて、それらを切り替えると、「goto fail」の例と同様に、複製された未テストのコードに対する明らかな許容度が再び見られます。概念実証テストが実施されている場合、アルゴリズム間のわずかな違いを実装するために、追加のパラメータ、おそらく小さな「ジャンプテーブル」を持つ1つの共通関数を抽出することで、重複を排除できるはずです。
ライナスの法則再考
「goto fail」バグとHeartbleedバグの両方が、ユニットテストが早期にキャッチするのに非常に適したタイプのエラーである、かなり簡単なプログラミングエラーであったことは、今では明らかでしょう。また、上記の議論から、両方の概念実証ユニットテストの実装によって裏付けられているように、これらのバグは、各バグを生成したチームがユニットテストの習慣を受け入れていれば、防ぐことができた可能性が高いことも明らかになっているはずです。
これらの壊滅的な欠陥は、「リーヌスの法則」—十分な目があれば、すべてのバグは浅い—の限界を示すと同時に、この法則の真の可能性を示しています。
十分な目があれば、悪用可能なすべてのバグが見つかります—ただし、必ずしも善意のある人によってとは限りません。
これらのバグが実際に悪用されたかどうかは不明ですが、コードはAppleとOpenSSLのサーバー上で長年オープンソースとして公開されており、悪意のある者がこれらのバグを発見し、誰にも通知せずにその知識を悪用する機会がありました。この認識を踏まえ、ライナスの法則の系を提案しましょう。
オープンソースコードのバグに気づくすべての目が、公共の利益のためにそれを報告したり修正したりする聖人であるとは限りません。
同時に、ソースコードへのオープンなアクセスを提供することは、両方のケースにおいて、インターネットアクセスがあれば誰でも事後にコードを検査して、エラーの本質と深刻さを把握し、技術的な詳細と影響について報告し、教訓と再発防止のための適切な対応について議論できることを意味しました。これらの報告の質は当然ながら様々ですが、オープンソースソフトウェアによってもたらされる透明性は、最終的には社会にとって有益となるであろう教訓につながるはずのオープンな議論を可能にします。同様の脆弱性がクローズドソースソフトウェアで発生した場合、この貴重な議論を行うことはより困難になります。実際には、同様の脆弱性が存在している可能性は十分にあり、ソフトウェア開発コミュニティ全体がそこから学ぶ機会を得ることはないでしょう。
オープンソースコードにアクセスできたことで、私は自分の単体/回帰テストをOpenSSLの中央ソースリポジトリに提出することができました。
また、両方のケースにおいて、オープンソースコードにアクセスできたことで、各コードベースに飛び込み、数時間で各バグの決定的な概念実証単体テストを作成することができました。また、OpenSSLの開発者と協力し、Heartbleedの概念実証単体テストのプルリクエスト(もちろん、GoogleからOpenSSLのコーディングスタイルに適応させたもの)を提出することができました。これは最終的にOpenSSLの中央ソースリポジトリにssl/heartbeat_test.c
として含まれました。
もちろん、これは疑問を投げかけます。なぜコードを担当するチームは、バグが導入された時点で、何年も前にそのようなテストを作成したり、強く要求したりしなかったのでしょうか?
責任はコードレビュープロセスにあり、そのプロセスで、正規のソースリポジトリへのアクセスを制御する開発者によって変更がコードベースへの組み込みが許可されます。コードレビュー担当者が単体テストを必須としない場合、不要なものが不要なものの上に積み重なり、「goto fail」やHeartbleedのようなものが紛れ込む可能性が増加します。おそらく「goto fail」の場合と同様に、多くの企業の開発チームは、高レベルのビジネス目標に焦点を当てており、コード品質を向上させるための直接的なインセンティブがなく、コード品質への投資は納期厳守と相反すると認識しています。Heartbleedの場合と同様に、多くのオープンソースプロジェクトはボランティア主導であり、中央の開発者は、各コード変更に徹底的で丁寧に作成された単体テストを添付するというポリシーを施行するための時間やスキルが不足しています。誰も彼らに高いレベルのコード品質を維持するよう報酬を支払ったり、圧力をかけたりしていません。
その結果、バグを生み出した開発文化では、単体テストを全く考慮していなかったか、考慮した上で、何らかの根拠でそれを拒否していました。それは私が考えるに、認識された「機会費用」として説明することができます。これは、単体テストが投資に見合う十分な価値を提供せず、他の優先事項や機会から貴重なリソースを奪っていると見なされたことを意味します。これは意識的な決定ではなかったかもしれませんが、チームが代わりに採用することにした他のツールや慣行によって、その選択は明らかになっています。
しかし、そのような決定がどのように行われたとしても、高機能な単体テスト文化を開発し維持することがコストのかからない提案ではないのは事実です。次のセクションでは、それらのコストを検討し、それらが価値があるかどうかを検討します。
ユニットテスト文化のコストとメリット
単体テストは、「goto fail」やHeartbleedのように注目度が高く影響の大きい欠陥を含む、低レベルの欠陥の数を大幅に削減し、コード品質や開発プロセスの他の側面にプラスの影響を与えることができますが、単体テスト文化を構築および維持するにはコストがかかります。タダ飯はありません。
初期費用
学習曲線が存在します。プロセスを暗記するのではなく、職人技に依存するあらゆるスキルと同様に、単体テストの書き方を学ぶプログラマーは、学習と発達、試行錯誤、内省、実験、統合の段階を経る必要があります。これには、他の活動から時間、エネルギー、資金を奪います。人々がこの慣行に慣れるにつれて、開発の初期段階では速度が低下します。
とはいえ、これは一度限りのコストです。チームにすでに良好な単体テストのプラクティスが確立されている場合、単体テストを誰かに習得させるコストは比較的低く、単体テストのスキルはプロジェクトからプロジェクトへ持ち運び可能です。したがって、学習曲線は、単体テストのプラクティスがまったくないチームにとって最も急峻になります。
単体テストは、他のツール、言語、プロセスと同様に、特に最初に始める場合、そして特に従うべき良い例や指導してくれるメンターがいない場合は、うまく適用できない可能性があります。脆弱で、大きく、遅く、常に壊れていて(その後無視される)不安定な単体テストは、ウイルスのようにテストスイート全体に複製される可能性のある悪い例を示しています。出来の悪いテストは、実際にはテストがないよりも悪く、テストは時間の無駄であるという印象を与える可能性があります。ビルドは壊れたまま無視され、絶え間ない失敗のノイズでテスト信号が溢れます。テスト環境での作業に興味のない開発者は、遅くて苦痛な変更を行うことを恐れることを受け入れるようになります。最終的な結果は、生産性の低下、欠陥のリスクの増加、そしてテストは他の人のためのものであると確信するチームです。
トレーニング
この知識と経験の不足を解消するために、意欲的な開発者は互いの単体テストスキルを向上させ、時間の経過とともにコードベースのテストカバレッジを増やすために団結することができます。このセクションでは、Google Web Server Teamがどのようにテストカバレッジを構築し、全体的な生産性を高めたかについて説明します。後のセクションでは、Google全体がどのように単体テスト文化を採用できたか、そしてその経験から得られた教訓が個々のチームにどのように適用できるかについて説明します。ただし、自主トレーニングには時間とエネルギーがかかり、全体像での見返りはすぐには明らかにならない可能性があるため、最後まで見通すには忍耐、誠実な努力、およびコミットメントが必要です。ただし、時間の経過とともに、コードベースが拡大し、より多くの開発者がチームに参加するにつれて、その価値はますます明確になります。2人チームは単体テストなしで管理できるかもしれませんが、20人チームは、機能とコミュニケーションの複雑さが複合されるため、より困難になります。
開発者が利用可能な資料を調査してスキルを向上させる意欲がない場合、または単に開始方法がわからない場合、これは内部トレーニングプログラムに投資するか、外部のヘルプを契約してトレーニングを提供する必要があることを意味する可能性があります。リソースが不足し、締め切りが迫っており、将来のメリットが明確ではない場合、これは少し価格ショックにつながる可能性があります。必要なスキルを習得するために必要な時間は、他のスキルやテクノロジーで開発者をトレーニングするために必要な時間よりも長くする必要はありません。しかし、開発者が抵抗した場合、プロセスはより長く、苦痛で、費用がかかる可能性があります。
袋小路に追い込む
テスト自体がメンテナンスの負担になることがあります。プロジェクトを窮地に追い込み、進捗を最大化するのではなく制限しているように見えるかもしれません。これは、単体テストの経験がなく、その価値を理解していない新しいチームにとって特に危険です。モックオブジェクトは、経験の浅い実践者による誤用の影響を受けやすく、価値があいまいな脆弱なテストにつながります。経験を積むにつれて、このシナリオは起こりにくくなります。最終的には一歩下がって、コードとテストの目標を再評価し、一方、他方、または両方を書き直すことを学びます。その間、過度に制限的なテストを修正するよりも、それを置き換える必要がある場合があります。
新しいプロジェクト、チーム、企業、またはドメインについて言えば、アジャイルプラクティスを文字通りに守り、常に純粋なテスト駆動開発(TDD)を実践することが理想的ですが、開発者またはチームが期待と動作を定義することに真剣に取り組む前に、探索し、遊び心を持つ必要がある場合があります。(常にすべてのアジャイルプラクティスを文字通りに守ることは、アジャイルを理解していないことの証拠だと主張する人もいます。)プロジェクトでできるだけ早くテストの経験を得ることが常に望ましい一方で、使い捨てのプロトタイプコードを作成する必要がある場合があります。その場合、徹底的な単体テストはおそらくやり過ぎです。これは、できるだけ早く製品をリリースしようとしているスタートアップに特に当てはまる可能性があります。
一方で、「使い捨てのコードほど永続的なものはない」という言葉に注意してください。トレードオフとして、テストを伴わずに実装される機能が多いほど、チームは後で返済しなければならない技術的負債を積み重ねることになります。最初からテスト容易性を考慮した設計(依存性注入の使用、1つのことに焦点を当てた明確なクラスの作成など)を行わない場合、ユニットテストは困難になる可能性があります。そのような負債の許容範囲を判断し、保守や新機能開発が煩雑になりすぎたときに、より高価な書き直しを避けるために、いつ返済する必要があるかを判断するのはチーム次第です。
誰がテストをテストするのか?
ユニットテスト自体がバグがないという保証はありません。この例(Google Testフレームワークに基づいたC++のような擬似コード)を考えてみましょう。
TEST_F(FooTest, IfAPresentFilterB) { setup input and add "A:" , "B:" run call EXPECT_TRUE(PresentInOutput("A:")) EXPECT_FALSE(PresentInOutput("B")) }
このテストの2番目の期待値は、"B"
だけでなく、コロン付きの"B:"
をチェックする必要があります。テスト対象のコードが誤ってコロンなしで"B"
をフィルタリングした場合、テストは失敗すべきときにパスしてしまいます。
この場合、テストがセキュリティの誤った感覚を提供し、事態を悪化させていると主張することもできます。しかし、テストが書かれていなくてもバグは存在する可能性がありました。バグのあるテストが存在する場合、コードとテストを修正することは、バグに対する回帰テストを提供することと同義です。テストを修正し、間違いから学ぶことは価値があります。テストを責めて削除することは後退です。将来的にバグのあるテストを回避するための1つの可能な対策として、そのようなバグを担当するチームは、将来のコードレビューの一部として提出されたテストコードをより詳細に検討し、「本番」コードと同じ優先度と注意を払うように努めることができます。
実際には、バグのあるユニットテストは例外である傾向があります。純粋なテスト駆動開発を実践している場合、テストをパスさせるコードを記述する前に、失敗するテストを記述する必要があります。これは、そのようなバグを防ぐのに役立ちます。純粋なTDDを実践していない場合は、テストが失敗することを確認するために、テスト対象のコードに一時的にエラーを追加することも役立ちます。いずれの場合も、コードがすべきでないことをチェックする(すべての入力が有効なハッピーパスをチェックするだけでなく)複数のテストケースを作成すると、他のテストケースのバグが明らかになる可能性があります。それでも、ユニットテスト自体にバグが含まれている可能性は残ります。特に、失敗すべきときに失敗するように注意を払わない場合です。
テストは愚か者のためのもの
過去には、世界を変えるコードを叩き出す、成功したチームやロックスタープログラマーでいっぱいの企業がありました。Googleは、その最初の数年間は確かにこの説明に当てはまりました。その場合、その時代には、ユニットテストに費やす時間は無駄だったと主張することができます。特に、ユニットテストを書くことに慣れていなかった場合、一流の開発者の速度を不必要に遅らせた可能性があるためです。会社とコードベースが小さく、コードレビューがすでに必須だったため、会社は、その環境で迅速に習熟できる「最も賢い」プログラマーのみを採用することで、複雑さを効果的に管理できました。
では、なぜその状態が永続的に続かなかったのでしょうか?
Googleのウェブサーバーの物語
リスクとコストにもかかわらず、ユニットテストのメリットは、壊滅的なバグをリリースする可能性を最小限に抑えるだけではないということを認識することが重要です。
私が2005年にGoogleに入社したとき、それはすでに非常に成功しており、多くの「古参」は、すべてを正しく行っていたからだと信じていました。その結果、当時から数年間、変化に対する抵抗が大きかったのです。しかし、ユーザーベースと壊滅的な事態の可能性が爆発的に増大し、成功とそれに伴う成長がGoogleに追いついてきたため、「ロックスター」が「ロックスター」コードを生成することは、長期的にはノイズと混乱を生み出すだけであることが明らかになりました。新しいGoogle開発者の流入は、これらの新しい開発者がアイデアを受け入れやすく、テストが最終的にこれらの新しい人々が習熟し、間違いを避けるのに役立つことが証明されたため、ユニットテスト採用への文化変革を加速するのに役立ちました。
具体的な例として、インターネットで最も人気のあるページであるGoogleのホームページを取り上げましょう。Google Web Server(GWS)チームのユニットテストに関する話は、会社全体でよく知られるようになりました。GWSチームは2000年代半ばに、Googleのホームページや他の多くのGoogleウェブページを提供するC++アプリケーションであるウェブサーバーへの変更が難しい状況に陥っていました。この困難にもかかわらず、新機能の統合は、Googleがビジネスとして成功するために不可欠でした。可能な限り迅速な変更を妨げていた障壁は、ほとんどの成熟したコードベースで変更を遅らせるものと同じでした。それは、変更がバグを引き起こすという非常に合理的な恐れです。
恐怖は心の殺し屋である。新しいチームメンバーがシステムを理解していないために変更を止め、経験豊富な人がすべてを理解しすぎているために変更を止めます。
Google Web Serverチームは厳しい態度をとりました。ユニットテストを伴わないコードは受け入れられませんでした。
この恐怖を克服しようと決意したGWSチームは、テスト文化を導入しました。彼らは厳しい態度をとりました。ユニットテストを伴わないコードは受け入れられず、コードレビューは承認されませんでした。これは、機能を開始しようとしている他のチームからの貢献者をしばしばイライラさせましたが、GWSチームは自らの立場を貫きました。
時間が経つにつれて、ユニットテストのカバレッジと開発の勢いが増加する一方、欠陥、本番環境のロールバック、緊急リリースの数が減少しました。新しいチームメンバーは、既存のテストが予期しない副作用を検出する可能性が高いという自信を持って、システムを1ユニットずつ深く理解し、変更の貢献を開始できるため、はるかに迅速に生産的になることに気づきました。彼らの初期の努力の過程で失敗させたテストは、システムの理解を加速させました。変更に慎重になり、貢献者からの変更を受け入れることに慎重になっていたチームの経験豊富なメンバーは、同じ理由で迅速に変更を行い、受け入れることができ、数時間または数日単位のフィードバックサイクルを伴う大規模で高価なシステムテストまたは手動テストに主に依存する必要がなくなりました。新しい開発者をさらに追加することで、チームは実際に動きを速め、より多くのことを行うことができるようになり、ブルックスの法則で説明されているように「遅れているソフトウェアプロジェクトに人員を追加するとさらに遅れる」というシナリオを回避しました。
さらに、恐怖の軽減により、高優先度のバグの慢性的な発生に妨げられることなく、刺激的な新しいマイルストーンに向けて具体的な進歩が見られるようになったため、プログラミングの喜びが拡大しました。創造的な流れの状態を維持できることによる高い士気が生産性に与える影響は、誇張できません。私がGoogleにいた間、GWSチームは、外部の貢献者からの膨大な数の複雑な変更を統合しながら、独自の継続的な改善を行うという、理想的なテスト文化を示しました。
GWSの例がテストグループレット(ユニットテストの採用を促進するために自発的に活動している開発者のチーム。この記事の後半で説明します)の努力を刺激したおかげで、Googleの多くのチームはユニットテスト文化に移行し、恐怖の軽減と生産性の向上から恩恵を受けることができました。慣性、無関心、時代遅れのツールの摩擦、そして抵抗を克服するには時間がかかりました。最初はユニットテストがコストのように感じられ、一部の人は、行動の2番目の表現を記述するのに費やす時間を新しいコード(昇進につながるコード)を記述するのに費やすことができるのではないかと心配していたためです。やがて、変化への恐れを捨てることがどういうことかを経験するにつれて、この副作用が、幸福、チームの幸福、および生産的な成果の収益性への影響という点で、コードの行数を容易に上回るものであることに気づきました。
緊密なフィードバックループ
時間が経つにつれて、ユニットテストの規律により、Google Web Serverチームはより速く移動し、より多くのことを行うことができるようになりました。ユニットテストは、バグを捕捉するのと同じくらい、生産性を向上させるためのものです。
見逃した場合は、GWSチームの話の重要な点は、時間が経つにつれて、ユニットテストの規律により、チームはより速く移動し、より多くのことを行うことができるようになったということです。ユニットテストはバグを捕捉するのと同じくらい生産性を向上させるためのものであるため、適切なユニットテストは彼らを遅らせるのではなく、スピードアップさせました。この結果に貢献したいくつかの要因を強調しましょう。
ユニットテストは、統合テスト、システムテスト、またはインターフェース契約のみに基づいてシステムを実行しようとする、あらゆる種類のアドバーサリーな「ブラックボックステスト」と同じクラスではありません。これらのタイプのテストは、ユニットテストと同じスタイルで自動化でき、おそらく同じツールとフレームワークを使用することさえでき、それは良いことです。ただし、ユニットテストは、特定の低レベルのコードユニットの意図をコード化します。それらは焦点が絞られており、高速です。開発中に自動テストが中断された場合、責任のあるコード変更が迅速に特定され、対処されます。
この迅速なフィードバックサイクルは、複雑な問題を解決するために必要な焦点とモチベーションの理想的な状態である、開発中のフローの感覚を生み出します。それとは反対の現象を、コンテキストスイッチングという使い慣れたオペレーティングシステムのメタファーを使用して比較してください。コンテキストスイッチングでは、操作の現在の状態を何らかの方法で保存し、新しいアクティビティを開始する前に、操作の新しい状態をスワップインする必要があります。次に、切り替えを元に戻すのにかかる時間と労力があります。さらに、操作ごとに管理する必要がある状態の量という問題もあります。ユニットテストがない場合、奇妙なコーナーケースや奇妙な副作用を記憶するためにより多くの脳を使用する必要があるため、コンピューターよりも得意なこと、つまり、すでに解決されているすべての問題の重さをやりくりするのではなく、新しい問題に対する解決策を進めるための時間とエネルギーが少なくなります。
言い換えれば、コードをより迅速に反復処理できるため、生産性を向上させることができます。ユニットテストを実行するだけで済む場合は、重いサーバーを起動する必要はありません。したがって、コードを正しく理解するのに数回試行が必要な場合、サーバーを何度も起動する必要がある場合は数分(またはそれ以上)かかる可能性がありますが、ユニットテストを毎回再実行するだけでよい場合は数秒で済みます。
コード品質の向上
ドッグフーディングが製品レベルで良い習慣であるように、自分のコードを使用するコードを記述する必要があることは、改善された設計につながる可能性があります。
学術的な純粋さの練習とはかけ離れて、コードの品質は重要です。悪いコードはバグが隠れるのに十分な影を提供します。良いコードは、バグが早かれ遅かれ発見され、撲滅される可能性を高めます。コードの作成者がそのコードのテストを記述すると、作成者は事実上最初のユーザーになります。自分のドッグフードを食べることが製品レベル全体で優れたソフトウェア開発の習慣であるように、自分のコードを使用するコードを記述する必要があることは、より読みやすく、保守しやすく、デバッグしやすい、改善された設計につながる可能性があります。
記述しているコードで解決しようとしている問題を考え、次に、クライアントとして、ソリューションを利用するために記述したいコードを考えます。その理想的なクライアントコードは、開発しているコードのインターフェースを使用するユニットテストケースとして表現できます。
コードレベルの設計がこのようにアプローチされると、より大きなシステムを構成するすべての小さな部分は、信頼性が高くなるだけでなく、理解しやすくなります。これにより、特定のコードが何をするかを理解するために必要な精神的な労力が最小限に抑えられ、誰もがより生産的になります。
実行可能なドキュメント
ユニットテストの名前は、コードの動作仕様として機能します。テスト自体は、各動作ケースのコードサンプルとして機能します。これを実現するには、テストコードにもプロダクションコードと同じ品質基準を設定してください。
適切に記述されたユニットテストは、2種類のドキュメントを提供できます。テスト名はコードの動作仕様のような役割を果たし、テスト自体は各動作ケースのコードサンプルとして機能します。一般的なアプリケーションプログラミングインターフェース(API)のドキュメントよりも優れている点は、きちんとメンテナンスされたユニットテストは、定義上、実際の動作の最新表現であるということです。ユニットテストの作成者は、他の開発者に対して、コードがどのように使用されるべきか、また、そこから何を期待できるかを効果的に伝えます。これらの「他の開発者」は、チームに新しく加わったばかりの人かもしれませんし、まだ雇用されていない(または生まれてすらいない)人かもしれません。このようなドキュメントは、ユニットテストがない場合に比べて、他の人に中断を求めずに、開発者が不慣れなコード、さらにはシステム全体を理解するのに役立ちます。
質の低いユニットテストにはこの品質が欠けています。これは通常、「プロダクション」コードよりもテストコードに対する考慮が少ないためです。解決策は、テストコードにもプロダクションコードと同じ品質基準を設定することです。そうしないと、テストの保守が困難になり、チームの生産性が低下します。
理解の加速
テストが失敗するたびに、システムへの理解を深める機会となります。
こう考えてみてください。テストが失敗するたびに、システムへの理解を深める機会となります。チームに新しく加わったばかりの人が、システムの変更を開始する際に多くのテストを失敗させることは、それぞれのイベントがシステムの認識を現実により近づけるため、生産性をより迅速に向上させるのに役立ちます。チームに長く在籍している場合、既存のテストは、新しい貢献者が抱く可能性のある多くの疑問に答え、あなたの時間と集中力を節約します。また、過去に書いた可能性のあるコードのすべてのニュアンスを思い出させてくれます。もし、再びそのコードに深く関わらなければならない場合に、しばらく考える必要がないでしょう。言い換えれば、よく練られたテストスイートをコードに追加すると、将来の自分のためになり、以前の状態にコンテキストを切り替えるのに必要な時間を最小限に抑えることができます。
GWSのユニットテスト導入前の時代を考えてみてください。十分なユニットテストカバレッジがないプロジェクトでは、何が壊れるかわからないため、何かをすることが怖くなります。
バグハンティングの高速化
統合テストまたはシステムテストでバグが見つかった場合、または新しいリリースがデータセンターにプッシュされた後、あるいはそのしばらく後にユーザーによってバグが発見されたと想像してください。バグのあるコードを担当する開発者は、すでに他のタスクに移っており、納期に追われている可能性があります。バグが十分に深刻な場合、少なくとも開発者の1人が対処するために中断する必要があり、進行中の新しい開発作業が遅れます。
バグのあるコードが、自動テスト、特に小規模なユニットテストのスイートによって十分にカバーされている場合、この中断はバグ修正を担当する開発者の時間をあまり必要としない可能性があります。既存のテストは、影響を受けるコードの意図に関するドキュメントとして機能します。開発者は、バグを再現するための新しいテストを追加し、修正を試みる前に欠陥が十分に理解されていることを検証します。この新しいテストはバグに対する修正を検証し、既存のテストは、修正に意図しない副作用がないことを高い信頼度で保証します。新しいテストは、回帰に対する保護のためにテストスイートの永続的な一部となり、修正がリリースされ、新しいリリースの開発が継続されます。中断は終了します。
バグのあるコードがユニットテストで十分にカバーされていない場合と比較してください。開発者は、影響を受けるコードを理解するために時間を費やし、エラーを特定し、その修正に副作用がないことを確認するためにより多くの注意を払う必要があります。修正の検証は、もしあれば、リリース前のテストの性質によっては、数日またはそれ以上かかる場合があります。中断は長引き、新しいリリースからより多くの開発時間とテスト時間を消費します。
さらに悪いことに、チームは他の何かを壊すことを恐れて、バグをそのままにしておくことを決定するかもしれません。それは間違いなく、ユーザーの信頼、ましてや開発者の自信と生産性を鼓舞しません。
経験はありますか?
これらすべての言葉、言葉、言葉の後でも、ユニットテストの価値と力に納得できないままですか?あなたを責めることはできません。正直に言うと、人生における他の良いものと同様に、実際に試してみるまで、それがどのようなものかを知ることはできません。それに加えて、誰かがあなたがそれをうまく行う方法を学ぶのを手伝うまで、あなたはそれをまったく楽しめないかもしれません。
私自身のユニットテストの経験は、広範な合理的な議論や、試してみることを納得させる説得力のある客観的な証拠から始まったのではありません。私がノースロップ・グラマンに所属していたチームは、必須の認証期限を満たすための厳しいプッシュを終えたばかりでした。その後の数か月間、パフォーマンスと安定性の理由でサブシステムを書き直している間、私は気まぐれにユニットテストを試してみました。2つの経験の違いは、これ以上ないほど異なり、非常に説得力のあるものでした。新しい機能が追加されるたびに、新しいシステムの進捗状況を視覚的に、そして実感することができ、完成した製品は意図したとおりに完成しました。まれにバグが発生した場合でも、それを特定、再現、修正、修正をリリースするのに数時間しかかかりませんでした。その過程で新しい欠陥を追加することはありませんでした。
ユニットテストを支持する最大の議論は、実際にユニットテストを経験することに勝るものはありません。何よりも、ユニットテストのスキルは、他の基本的なプログラミングスキルと同様に、ドメイン、言語、企業間で移植可能です。
私が言いたいのは、ユニットテストを支持する最大の議論は、実際にユニットテストを経験することに勝るものはないということです。生産性を測定することはできませんが、それを感じることはできます。最初のユニットテストが醜く、複雑で、もろいものであったとしても、私を信じてください。あなたはそれを改善することができ、その報酬は旅に値するでしょう。
何よりも、ユニットテストのスキルは、他の基本的なプログラミングスキルと同様に、ドメイン、言語、企業間で移植可能です。それは生涯にわたって利益をもたらす投資です。覚えておいてください。過去のユニットテストの経験が、「goto fail」とHeartbleedの両方の概念実証ユニットテストを非常に迅速に記述することを可能にしたのです。コードに精通しておらず、長年定期的にプログラミングを行っていなかったにもかかわらずです。
手を汚せ
この記事の最初の2つのセクションには、「goto fail」ユニットテストバンドルとHeartbleedユニットテストへのリンクが含まれています。まだ行っていない場合は、コードをダウンロードし、ビルドして、システムで実行してください。テストが合格することを確認してください。次に、テストコードまたはテスト対象のコードのいずれかを変更して、テストを失敗させます。出力を見てください。それを受け止め、熟考してください。次に、コードを修正して、テストを再度合格させます。
「goto fail」とHeartbleedのコードを理解したと思っていたかもしれませんが、実際にそれがどのように機能するかを実感できたはずです。
あなたが(経験したはずの)経験は、実際のシステムの一部を変更し、製品全体を構築して起動したり、ユーザーインターフェースを調べてみたりすることなく、ほぼリアルタイムでその変更の影響を確認することから得られる知的なスリルです。考えてみてください。今まで、あなたはこの記事の前半の説明や、その他の読んだかもしれない情報源を読んで、「goto fail」とHeartbleedのコードを理解したと思っていたでしょう。しかし、今ではコードが実際にどのように機能するかを実感することができました。Heartbleedテストの場合、マシンのメモリの内容が画面にこぼれるのを実際に確認できました。(私のマシンでは、PATH
やその他の環境変数がはっきりと見えます。)
追加または変更したコードが、意図したとおりに機能したことをすぐに検証する喜びは、それ自体が報酬です。自分のコードが、投げ込まれる可能性のあるあらゆる入力を正しく処理するという(相対的な)確信感は、爽快です。テストで、記述したばかりのコードのエラーが検出されたときの興奮、それはあなた(または他の誰か)が、後で何時間もデバッグ、修正、検証、クリーンアップを行う必要がないエラーであり、中毒性があります。
そして、見たことのないコードの主要なバグを再現することですか?それはプライスレスです。
私たちがユニットテストを支持するようになったのは、即時的な満足感が最大の理由です。合理的な議論、データ、グラフ、金額は必要ありません。
その即時的な満足感こそが、ユニットテストを支持する私たちのほとんどを夢中にさせたものです。他の人にとっては、回帰が発生しないという高い信頼感です。どちらの場合も、ユニットテストは、進歩感、恐れを知らない生産性に基づく純粋な高揚感を生み出し、他の依存症の不快な副作用はありません。合理的な議論、データ、グラフ、金額は必要ありません。
テストは孤立したものではない
しかし、そのすべてのメリットにもかかわらず、ユニットテストは、高品質でほとんどバグのないコードを確保するための開発ツールボックスの唯一のツールであってはなりません。次に、日常の開発の一部としてユニットテストと組み合わせて使用できる他の多くの利用可能なツールとプラクティス、およびこれらの他の項目に照らしてユニットテストを採用する価値がまだある理由を見てみましょう。これらの他の項目は、欠陥を早期に発見するために利用できます。
その他の役立つツールとプラクティス
プログラミングエラーを早期に検出するユニットテストの有効性、およびその他の生産性の利点にもかかわらず、それは万能薬、リリース前にすべてのソフトウェア欠陥を確実に排除することが保証されている奇跡的な治療法からはほど遠いものです。ソフトウェアにバグがないことを保証するツールを開発することは、停止問題を解決することに相当するため、どのツールも効果的にそうすることはできません。有名な引用句の通り
...プログラムのテストは、バグの存在を示すためには非常に効果的に使用できますが、バグがないことを示すことは決してできません。
確かに、一部のクラスのバグは、少なくとも一般的な意味では、効果的なユニットテストが困難であることが知られています。これの輝かしい例は、競合状態やデッドロックなどの共有された変更可能なメモリの結果として発生する可能性のある並行処理バグのクラスです。これには、単一のプログラム内でデータを共有するスレッド、ディスク上のファイルを共有する同じマシン上のプロセス、またはデータベースに保存された情報の整合性を確保する必要がある分散システムが含まれる可能性があります。単一スレッドコンテキストの各ロジックビットをユニットテストすることは、マルチスレッドコンテキストでの正確性を保証するための第一歩ですが、並行処理バグがないことを保証するには十分ではありません。このような問題を検出してデバッグするには、他のツール、テストのレイヤー、ステージング環境、および監視とロギングの形式が必要です。(ただし、このようなバグが発見された場合は、それを確実に再現して修正を検証するためのユニットテストを提供することが理想的です。)
これらの真実に照らして、早期に欠陥を検出するという問題に対処するために、他のツールやプラクティスを活用することは賢明であるだけでなく、コードの品質を高め、製品の成功の可能性を最大限に高め、失敗の可能性を最小限に抑えるために不可欠です。ただし、他のツールを検討する際には、次の点を覚えておいてください。この記事の前のセクションの概念実証テストで示されているように、ユニットテストは、既存の開発ツールを使用して、他の開発プラクティスのコンテキストで、既存のコードに対して、任意の言語で適用できます。特定のツール、フレームワーク、言語、プラクティスによってユニットテストがより簡単かつ生産的になる場合がありますが、必須ではありません。主なコストは、ユニットテストについて開発者、管理者、幹部を教育し、開発者にユニットテストを行うことを納得させるためのものです。
真の魔法は、ユニットテストや他のツールが連携して使われるときに起こります。コードの作成と保守を容易にするのと同じツールとプラクティスが、ユニットテストの作成と保守も容易にします。同時に、テスト容易性を考慮した設計は、適切に行われ、論理的な極端に走らない限り、レビュー、保守、拡張、デバッグ、他のツールでの分析、ドキュメント化が容易なコードになることが多いです。すべてのツールとプラクティスには長所と短所があり、開発文化に統合された各ツールとプラクティスは、バグが製品に紛れ込む可能性を減らし、発生した場合の対処に必要な時間と労力を削減します。
プログラミングは職人技であり、ほとんどの職人技と同様に、熟練者は仕事に適したツールを選択し、個々の製品に必要なカスタムツールを作成する達人です。建設業者がセメント基礎を敷設する場合、建設業者が最初に作成するのは、その基礎を保持し形作る木製の構造物です。熟練した大工は、最初にすべての部品を所定の位置に保持するフレームワークを構築するかもしれません。ソフトウェアでも同じことが当てはまります。アプリケーションを作成する際、開発環境の基盤となるプラットフォーム、言語、ツールを選択し、次にそのアプリケーションに必要なカスタムツールを構築します。つまり、部品を分離してそこに注意を集中できるようにするスタブとフェイク、そしてコードを精査できるように照らすユニットテストです。仕事に適したツールがなければ、不十分な結果になるリスクは依然として高いままです。
静的解析/コンパイラ警告
静的分析とコンパイラの警告は、十分にテストされたコードにも適用できる優れたツールです。異なる視点からコード品質を確保する補完的な安全対策は、これらのツールが既存のテストでは見逃されている問題箇所を強調する可能性があるため、常に良いアイデアです。それでも、ユニットテストは、機械が決して文句を言わない可能性のある問題を明るみに出すことができます。「goto fail」バグは、静的分析または到達不能コードのコンパイラの警告で検出できた可能性があります。しかし、警告または必須の中括弧でこの1行のコードが欠陥を生み出すのを防ぐことができたとしても、ユニットテストの文化があれば、コードを担当する開発者がそのカバーを提供していた重複を根絶することを促したでしょう。新しい関数を抽出してこの重要なアルゴリズムを徹底的に実行するテストを作成した後、プログラマーは同じファイルに表示される同じアルゴリズムの6つのコピーすべてを、単一の分離された関数への6つの呼び出しに置き換え、長期的なコード品質を向上させ、長期的なバグが忍び込んで隠れる可能性を減らすことができました。
静的分析ツールは重複コードの検出が上手になっていますが、ユニットテストを作成するプログラマーは依然として最初かつ最も効果的な防御線です。また、このようなツールはデッドコードを検出できますが、もし「goto fail」が誤ったマージの結果だった場合、マージエラーが代わりにこうなっていたらどうでしょう。
- if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) - goto fail;
つまり、マージの結果が、アルゴリズムの最後のステップを誤って削除することだった場合です。同じバグですが、世界中のすべての中括弧、静的分析、コンパイラの警告は役に立ちません。ユニットテストで検出できたはずです。(概念実証ユニットテストでこれを自分で試すことができます。)
それでも、静的分析とコンパイラの警告は、テスト対象のコードとテスト自体における典型的なエラーを検出するのに役立ちます。特にコンパイラの警告は、既存のツールチェーンに既に組み込まれているため、適用が最も簡単なツールの1つです。
プログラマーは、このようなツールを初めて適用すると、「誤った警告」について不満を言うことがあります。なぜなら、圧倒的な出力の怒涛をもたらす可能性があるからです。実際、一部のツールは、レースコンディションや、検査時に見かけ上偽であるように見えるnull/nilポインターなどの問題について不満を言う可能性があります。ツールがまだ成熟していないか、特定の製品に適用できない可能性があります。通常、静的分析ツールでは、特定の警告を抑制できるようにすることで、チームが特定の警告を無視するか、一時的に非表示にするかをケースバイケースで決定できます。
問題が山積みになっている場合は、問題に取り組みましょう。漸進的な進歩は、機能開発だけのものではありません。
一方で、多くのツール、特にコンパイラの警告は、比較的少ないノイズで正当な問題を検出できます。また、潜在的な問題が長期間検出されなかった場合、それらは積み重なる傾向があり、ツールを適用すると大量の警告とエラーが発生します。この場合の真の解決策は、既存のコードにユニットテストを追加するのと同じように、問題に取り組むことです。1つのファイルの1つのクラスの警告を修正します。次に進みます。まだカバーされていない場合は、コードのテストを追加してみてください。漸進的な進歩は、機能開発だけのものではありません。
モダンな言語
CやC++の低レベルの効率を必要としない新しいプロジェクトやアプリケーションを開始する場合、Python、Ruby、Java、Scala、C#、Goなどの「モダン」な言語の方がより魅力的かもしれません。これらの言語には次の機能があるからです。
- ファーストクラスのオブジェクト指向プログラミング機能(例:継承/構成、カプセル化、ポリモーフィズム)。
- 自動メモリ管理。
- 配列境界チェック。
- 多くの低レベルプログラミングタスクのための共通ライブラリ。
この決定は、ユニットテスト文化の構築に投資するという決定とは、ほぼ直交したままです。そして、ほとんどのモダンな言語には、ユニットテストをサポートするために標準ディストリビューションに組み込まれている堅牢な機能とライブラリがあり、それらは役に立つはずです!
既存のプロジェクトの場合、ユニットテスト文化の構築に関しては、新しい言語への切り替えはほとんど必要ありません。「goto fail」およびHeartbleedバグはCで記述されたコードに属していましたが、概念実証ユニットテストが示すように、効果的なユニットテストは、より「モダンで安全な」言語に頼ることなく、そのようなバグをキャッチして拡散を防ぐことができます。既存のシステムを新しい言語で書き換えることは、費用がかかり危険なプロセスであり、何年もメリットが得られない可能性があります。それでも、それが価値がないとは言えませんが、ユニットテスト文化を開発することは、今日から始めることができ、そのメリットは知覚されるコストとリスクをはるかに超えます。これは、ユニットテストを既存のコードに段階的に適用できるためです。たとえ、コードが改善されたテスト容易性をサポートするために少しずつ更新する必要があったとしても、「goto fail」の例で示されているようにです。さらに、この記事の後半で説明するように、GoogleのTesting Groupletは、企業が大規模にこれを達成するのを支援し、既存のコードにユニットテストを追加することが解決済みの問題であることを決定的に証明しました。
チームがシステムを新しい言語で書き換えるリスクを冒す価値があると判断した場合、その言語は潜在的な欠陥すべてに対する解決策と見なすべきではありません。到達不能なコードや安全でないメモリアクセスだけが待っているバグではありません。書き換えは、機能が再実装されるときにユニットテストを追加する絶好の機会を提供します。言語が動的に型付けされている場合は、他の言語のコンパイラが自動的にキャッチするエラーに対して、予想される型を文書化し、防御するためのユニットテストスイートをさらに用意することが重要です。iOSからAndroidへの移植など、アプリケーションを新しいプラットフォームに移植するために新しい言語での書き換えが必要な場合、移植するユニットテストスイートを用意すると、移行をスムーズにし、移植エラーを防ぐのに役立ちます。
OpenSSLのような低レベルのシステムプロジェクトを書き換える場合、C++とGoはC以外に現実的な言語オプションの数少ない言語の1つです。これらの両方の言語で利用可能なテストツールとフレームワークは、Cで利用可能なものよりも強力であるため、ユニットテストがさらに簡単になります。Google TestとGoogle Mockは、同等のJavaテストフレームワークのパワーと柔軟性に匹敵します。Goには素晴らしいカバレッジツールが組み込まれており、ogletestはGoogle Testの影響を強く受けたフレームワークです。
オープンソース化
「goto fail」とHeartbleedによって証明されているように、コードをオープンソース化しても、定義上バグがなくなるわけではありません。上記の「リーヌスの法則」に関する議論で述べたように、オープンソースは社会に多くのポジティブなメリットを提供します。また、開発コミュニティや、潜在的な顧客や従業員からの多くの善意とサポートを得ることができます。しかし、オープンソースコードは、人気のある信念にもかかわらず、高品質でバグのないコードを自動的に保証するものではありません。
コードをオープンソース化することにした場合は、以前に提案された系に加えて、リーヌスの法則の別の系に従うのが良いでしょう。
コードをオープンソースとしてリリースする場合は、ユニットテスト済みであることを確認し、寄稿された変更には高品質のユニットテストとドキュメントを添付することを要求します。
ユニットテストが機能開発とともにファーストクラスのステータスを与えられた場合、「goto fail」とHeartbleedは回避できたはずです。さらに、不足しているテストケースを見つけたり、未カバーのコードに新しいテストを追加したりすることで、人々がプロジェクトの開発と長期的な健全性に貢献するのが容易になるでしょう。新しい開発者も、安全ネット、実行可能なドキュメントの一種、および理解を加速するフィードバックメカニズムとしてテストを持つことで、システムを把握しやすくなるでしょう。
スタイルガイド/コーディング標準
コードのレビュー担当者がコードをより詳細に精査する必要があるという手がかりを提供できるコーディング標準のセットを開発し、順守するのが遅すぎることはありません。スタイルガイドは、空白の使用、中括弧の配置、シンボルの名前に関する無数の潜在的な議論を回避するのに役立つだけでなく、特定の規則から逸脱したコードが欠陥の存在を示している可能性がある場合、プログラマーが使い慣れた視覚的な規則を通じて検出するのに役立ちます。コーディング標準はテストの必要性をなくすものではありません。2つのプラクティスは互いに補強します。繰り返しますが、2つの異なる表現で同じ間違いを犯すのは、1回犯すよりも難しいです。
ただし、スタイルガイドは多くのエラーを回避するのに役立ちますが、誤った論理条件や数学的演算を検出することはできません。ほとんどのコンパイラや静的解析器も同様で、テストされていないコードは、そのようなエラーをレビューするのがはるかに困難になる可能性があります。スタイルガイドだけでは、いわゆるSOLID設計原則を促進するような設計上の圧力をかけることはできません。テスト容易性を考慮した設計は、優れたオブジェクト指向設計と実質的に区別がつきません。
この記事の前のセクションでHeartbleedについて述べたように、OpenSSLプロジェクトは、リクエストバッファを処理する、または出力バッファを割り当てて書き込むすべてのコードに、共通のバッファ脆弱性から保護するためのテストを付随させる標準を採用する可能性があります。また、レビューのために提出されたすべてのコードが、新規または既存の単体テストでカバーされていることを要求することもできます。
コードレビュー
コードレビューは、ユニットテストの代わりではなく、理想的にはユニットテストに加えて採用する価値のあるプラクティスです。これにより、暗黙の前提を洗い出すのに役立ちます。私たちは皆、当たり前の知識を持っており、それが他の人には明白ではないかもしれないことに気づかないことがよくあります。レビュー担当者にコードを理解してもらう過程で、作成者は自分の前提を明示し、コードの意図をより明確にする必要に迫られることがよくあります。また、同僚が実際にコードを見て公然とコメントするという認識により、「物事を正しく行う」というモチベーションが高まります。これにより、コードの品質が向上し、バグが発見されることもあります。
コードレビューは、ドキュメント作成にも役立ちます。レビュー担当者のコメントを読むことは、ドキュメントを作成しようとしている人にとって、他の人にとって何が混乱を招くのかを明らかにし、見落とされていたかもしれない詳細を強調することで、非常に啓発的になります。コードレビューは、コードと一緒にドキュメントをレビューする良い機会でもあります。
開発者がレビュープロセスを合理化するには少し時間がかかる場合があり、コードレビューに費やす時間はコードを書かない時間になります。それはデバッグに費やす時間でもありません。しかし、知識の伝達がチームまたは会社全体のコーディング、ドメイン、製品に関する専門知識の向上につながる可能性は計り知れません。それが正式に文書化されたプロセスの一部として行われるか、ペアプログラミングの文書化されていない副作用として行われるかにかかわらず、ソース管理にコミットされたすべての変更はコードレビューされるべきです。
レビュー担当者が一度に調べるコードが少ないため、小さな変更の方がコードレビューが容易です。十分にテストされた変更は、作成者がどのようなケースを考慮したかを確認でき、さらに提案するように促される可能性があるため、コードレビューが容易になります。テストによってコードの変更全体が大きくなりますが、適切に記述されていれば、テスト対象のコードに対する変更を明確にするのに役立ち、レビューが比較的簡単であることが証明されるはずです。これらの2つの原則を組み合わせると、完全な機能に構築される比較的小さく、十分にテストされたコード変更は、Heartbleedバグを生み出した変更のような、テストのないモノリシックな変更よりもレビューが容易になります。レビュー担当者が一連の小さく、十分にテストされた変更を要求していた場合、レビュー担当者は、作成者が無効なユーザー入力を処理する際の弱点を調べ、それに対する防御策を講じたことを検証できた可能性があります。言い換えれば、テストはコードの品質を向上させるだけでなく、コードレビューの品質も向上させます。
この品質の向上は、「ユニット」が比較的狭い範囲であるため、読みやすく理解しやすいという事実に起因しています。優れたコードレビュープラクティスには、成功ケースと失敗ケースの両方が適切な単体テストでカバーされていることを確認することが含まれます。
統合/システムテスト
一部の人は、単体テストよりも統合テストまたはシステムテストを優先する必要があると主張します。確かに、大規模で複雑なプロジェクトの統合テストとシステムテストは不可欠であり、自動化すればするほど優れています。ただし、問題になっている2つの特定のバグが示すように、最悪のバグはシステムレベルで検出するのが最も難しい場合があり、ユニットレベルでテストするのが最も簡単な場合があります。ユニットテストは、開発者がコードの各行を記述し、変更をレビューに出す際のバグに対する最初の防御策である必要があります。ユニットテストは、他のレベルのテストでは実質的に実現不可能なコーナーケースとエラー処理ケースを実行できます。
要するに、ユニットレベルで捕捉できたはずのバグを、統合レベルまたはシステムレベルで捕捉するべきではありません。バグが通過した場合、それを再現し、リグレッションを防ぐために、可能な限り低いレベルでテストを作成します。より高いレベルで同等のテストを作成すると、テストが不必要に複雑になるだけです。
統合テストとシステムテストは、ユニットテストよりも桁違いに遅くなる可能性があります。それらは多くの場合、他のモジュールまたはシステム(アプリケーションの外部にあるものもある)と対話する傾向があります。これにより、テストに時間がかかります。テストが遅くなるほど、開発者に与える即時的な価値が低くなり、バグが導入される可能性が高くなります。テストが遅くなるほど、与える即時的な価値が低くなります。コードを開発しているときは、入力を完了した直後にエラーを知らせてくれるほど高速に実行されるテストが必要です。作業中の内容が頭に入っている間が、付随するバグを撃ち落とす絶好の機会です。今すぐ修正して、自信を持って進みましょう。
とは言え、統合テストとシステムテストは必要です。ユニットテストだけでは、高レベルのコンポーネント間の統合が失敗しないことや、システム全体がエンドツーエンドで完全な操作を正常に実行できることを保証することはできません。実際、製品の性質によっては、統合レベルのテストの方が書きやすく、実行もほぼ高速で、信頼性と保守性が十分に証明され、大きな価値を提供できる可能性があります。うまく行けば、統合テストはコンポーネント全体で積極的なリファクタリングを可能にします。既存の統合テストに変更を加える必要はほとんどなく、一部のユニットテストは、その過程で適合または書き直す必要がある場合があります。
テストが定義上純粋な「ユニット」テストであるか、迅速に実行される適切に制御された統合テストであるかにかかわらず、狭い範囲の自動テストは、他のツールやテスト層の隙間からすり抜ける可能性のある、壊滅的な低レベルのプログラミングエラーを検出できます。さまざまなサイズのテストのバランスが望ましいです。特定のサイズのテストがない場合は、トラブルを招くことになります。
ユニットテストは、実際には、より簡単で効果的な統合テストとシステムテストにつながる可能性があります。ユニットテストによるコード品質の向上により、適切に設計されたインターフェイスを備えた意味のあるコンポーネントのコレクションとして、システムの構成が改善される可能性があります。これにより、より高レベルのテストの基礎が改善され、これらのテストで問題が発見された場合のデバッグが容易になります。設計によってユニットテストではなくシステムテストを実行する必要がある場合は、設計を変更する必要があるという確かな兆候です。
ドキュメント
最終的には、価値のあるすべてのシステムにドキュメントが必要になります。これは、アプリケーションプログラミングインターフェイス(API)の低レベルの技術ドキュメントから、システム動作の高レベルのドキュメントまで多岐にわたります。これらのドキュメントは、コードまたはシステムが満たすことを目指す要件、契約を効果的に定義します。システムの一部をドキュメント化するのが難しい場合は、設計に何か問題があることを示す警告であることがよくあります。
ユニットテストやその他の自動テストは、実行可能なドキュメントの形式を提供しますが、コードを直接担当していないプログラマーにとっては、最もアクセスしやすいドキュメントではない可能性があります。ただし、優れたユニットテストと優れたドキュメントを組み合わせることで、高品質の製品を確保できます。優れたドキュメントは、コードまたはシステムの期待値を定義します。優れたユニットテストまたはその他の自動テストは、これらの期待値を検証します。一貫性のある設計に貢献する適切に記述されたテストは、より正確で一貫性のあるドキュメントにも貢献します。
ファジングテスト
ファズテストは言及に値する別のテストスタイルです。Codenomiconは、自社製品のファズテスト中にHeartbleedを発見しました。これには、エラーを発見しようとして、あるプログラムの入力を別のプログラムに自動的に生成するプログラムを実行することが含まれます。Heartbleedの発見は、とりわけその有効性を強く裏付けています。
これはもう1つの補完的なセーフガードです。ただし、ファズテストはユニットテストの代替にはなりません。ファズテストは既存のテストでカバーされていないケースを明らかにすることができますが、ユニットテストはファズテストが実行される前に多くのエラーを捕捉できます。ファズテストでエラーが見つかった場合は、リグレッションを防ぐために、適切な範囲で自動テストを使用してエラーを再現することが標準的な慣行である必要があります。
継続的インテグレーション
継続的インテグレーションは、コードベースのメインラインが常にリリース可能な状態であることを保証するために、常に更新、ビルド、テストするプロセスです。すべてのコード変更でプロジェクトを再構築するCIシステムは、コード変更がコンパイルエラーにつながった場合を検出するのに役立ちます。ただし、その目的のみに使用することは、その真の能力、つまりコード変更がテストの失敗を含む全体的なビルドの失敗につながった場合を検出する能力を著しく無視することになります。テストを実行しない継続的インテグレーションシステムは、食料品店への往復にのみ使用される頑丈なピックアップトラックのようなものです。確かに、それは重要な機能を果たすのに役立ちますが、それ以外にもできることはたくさんあります!実際、ビルドが自己テスト型でない限り、それは実際には継続的インテグレーションシステムではないと主張できます。
継続的インテグレーションシステムは、セットアップとメンテナンスに手間がかかる場合がありますが、それだけの価値があることが多いです。Jenkinsは、Javaで記述された人気のあるオープンソースのCIシステムです。Buildbotは、Chromium、WebKit、その他のプロジェクトで使用されている別のオープンソースCIフレームワークです。ThoughtworksのGo継続的デリバリーシステム(GoogleのGoプログラミング言語と混同しないでください)は、複雑な依存関係のパイプラインを管理でき、統合だけでなく製品を継続的にデプロイできる別のオープンソースシステムです。
この記事の後のセクションで説明するGoogleのテスト自動化プラットフォームは、Googleでの大規模開発のあり方を大きく変えた非常に強力なシステムでした。これは、会社全体から中央リポジトリに提出されたすべての変更に対して、数分以内に結果を提供する、大規模分散ビルドおよびテストインフラストラクチャに依存していました。Solano CIは、サポートされている言語の1つを使用して記述されたプロジェクト向けの、独自の分散CIサービスです。
クラッシュとコアダンプ
プログラマーがアサーションをコードに挿入し、プログラムをクラッシュさせ、言語と動作環境に応じて、データ破損、暴走プロセス、その他の危険を冒すよりも、スタックトレースまたはメモリイメージ(UNIX用語では「コアダンプ」)を生成することが一般的です。これは、プログラムのコードが単体テストされているかどうかにかかわらず、良い防御的なプラクティスです。ただし、プロセスをクラッシュさせることは最終手段である必要があります。コードが統合され、手動テストされ、ステージングエリアに事前に起動された後、または場合によっては本番環境で起動された後に、基本的なコーディングエラーを診断しようとするのは、単体テストを作成してそのようなエラーを事前に捕捉しようとするよりもはるかに費用のかかる命題です。他のプロセスが同じ入力で同じように停止した場合、問題が解決されるまでサービスの他のトラフィックを処理する能力が低下し、潜在的にビジネス、収益、および信頼の損失につながる可能性があります。
リリースエンジニアリング
リリースエンジニアリングは、特定のソフトウェアリリースに対するすべての機能、バグ修正、その他の入力を追跡し、すべての成果物にラベルが付けられ、アーカイブされ、最終製品がコマンドで再現可能になるように行うプロセスです。クラウドベースのソフトウェアの場合、本番環境への制御されたロールアウトを実行し、成功またはロールバックの必要性を示す本番環境監視信号に細心の注意を払うことも含まれます。RelEngは、バグが本番環境に入り込みユーザーに影響を与えることを防ぐための最後の防衛線となります。リリースエンジニアは、テスト全般、特に自動テストに対する最も強い信奉者です。自動テストに合格することが、リリースを進めるかどうかを判断する上で彼らが依存する最大のシグナルの1つであるためです。これは次の理由によるものです。
- 再現性:自動テストは、手動テストよりも本質的に再現性が高い
- 監査性:自動テストは、手動テストよりも監査可能な記録を多く生成する
- リリース自動化との統合:自動テストは、全体的なリリース自動化のほんの一部にすぎませんが、手動テストはそれを中断させる
サイト信頼性エンジニアリングと本番環境モニタリング
サービスがクラウドで実行されると、Web運用、またはGoogleの用語ではサイト信頼性エンジニアリングの領域になります。チームに専任のSREがいない場合、少なくとも1人の開発者がそのタスクを担当する必要があります。SREは、実行中のプロセスまたはプロセスのグループの外部から観測可能な動作を監視するツールに加えて、実行中のプロセスによってエクスポートされる変数と、これらの変数に基づいた計算を監視することにも大きく依存しています。エクスポートされた変数を、プロセスの健全性の「バイタルサイン」と考えることができます。
監視とSREサポートは必要かつ不可欠ですが、エラーを発見するための主要な手段であってはなりません。確かに、十分に複雑なシステムでは、エラーが時折発生するでしょう。しかし、SREにとって、時間とエネルギーを消費する本番環境での障害の数を最小限に抑え、各障害の解決にかかる時間を最小限に抑えることは利益になります。それは開発者にとっても利益になります。しかし、(一般的に言って)SREは基盤となるコードに対する関与が不足しているため、コードが開発者が思っているほど正しいという確信がないため、緊急の本番作業につながる言い訳にはるかに寛容ではありません。特に、午前3時や週末、休日に対応するために呼び出されるような作業には。
良いニュースは、標準の監視フックが便利なテストツールになる可能性があるということです。特別なインターフェイスやモックオブジェクトを介して内部動作を検証する方法を考案するのではなく、プログラムによってエクスポートされたカウンターやその他の監視変数をチェックすることで、あらゆるサイズの自動テストで簡単に検証できる出力を提供できます。
コスト
単体テストを含むこれらすべてのツールとプラクティスは、起動コストとメンテナンスコストが発生することを覚えておく価値があります。このコストは、お金がなく、ハードウェアがなく、正しいプロセスのドキュメントがほとんどなく、多くの場合、他のことに取り組む本業を持つオープンソースプロジェクトに貢献する個人にとって最も深刻です。Googleが持っているような開発者サポートチームがない限り、継続的なビルドを設定するには多大な労力が必要となることがよくあります。これらの推奨事項はすべてその観点から考慮する必要があり、組織全体でビルド/テスト/QAの多くの機能を集中化することを強く支持します。それでも、何もしないコストは、長期的には、コードの品質を高く保ち、欠陥を防ぐために適用できる単体テストやその他のすべてのツールを採用するコストよりも大きくなります。製品がユーザーベースの幸福にとって重要である場合は、そうしないわけにはいきません。
バランスの取れた朝食の一部
議論に値するツールとプラクティスはまだあります。防御的なプログラミング/契約による設計スタイル、バグレポートとユーザーフィードバックメカニズム、ログ記録とエラー検出および診断における役割、自動化されたスタックトレース照合および分析ツールなどです。これらのツールとプラクティスを、勤勉な単体テストプラクティスと組み合わせることで、コードが作成される時点、またはしばらく経ってから、コードの品質を大幅に向上させることができることは明らかでしょう。それらのすべてを検討する価値はありますが、単体テストを最初に採用すべきであるという説得力のある主張ができたと思っています。うまく行うには知識と経験が必要ですが、メリットを享受し始めるために、追加のツール、特定のプログラミング言語、または追加のプラクティスを必ずしも採用する必要はありません。さらに、既存のコードに段階的に追加して、コードの品質を向上させ、時間の経過とともに欠陥の発生を確実に減らすことができます。
この記事で以前にも述べたように、私はこれを経験したから知っています。Googleを辞めることを決める上で最も困難なことの1つは、おそらく二度とこのような開発環境を経験することはないだろうという認識でした。さらに、私は計り知れないほど誇りに思っていた成果から離れようとしていました。私のパートナーである犯罪者たちは、単体テストに対してほとんど無知、無関心、または敵対的であった開発文化全体で単体テストの採用を推進するのに貢献しました。次のセクションでは、Googleの開発環境の他のいくつかの要素とともに、私たちの努力の詳細を共有したいと思います。これらの要素は、大規模な高品質コードを実現するのに役立ちました。
Googleの改良されたテスト文化、または:デジャブの繰り返し
「goto fail」とHeartbleedバグを教育的な例にする最大の理由は、その可視性が高いこと以外に、このようなバグの検出と防止は解決済みの問題であるためです。私がGoogleに入社した当時、開発文化は単体テストに概して消極的でした。私が他の人々と一緒に行ったGoogleのテスティンググループレットの一部としての作業は、「私のコードはテストが難しすぎる」または「テストをする時間がない」と主張し、テストを書くことを例外ではなく、標準にするのに役立ちました。以下は、テスティンググループレットが、単体テストについて無知であるか、敵対的である開発者がほとんどである、大規模で成長している成功した企業で、強力な単体テスト文化をどのように育成したかの簡単な説明です。
また、私が在籍していた当時のGoogle開発環境の他のいくつかのコンポーネントについても触れて、Googleが大規模な規模と機能開発の速度にもかかわらず、どのように高いレベルのコード品質を維持していたかについて、より完全な全体像を提供します。この情報の一部は古くなっている可能性がありますが、私の記憶に基づく全体像は依然として役立つ可能性があると信じています。この説明は、保証されたプロセスを規定するものではなく、組織で同様の変更を行おうとしている他の個人やチームにインスピレーションを与えることを目的としています。
テスティンググループレットの活動とすべてを成功させた人物についてのより完全な情報は、私のブログのテスティンググループレットタグページをご覧ください。
抵抗
Googleは、無限のリソースと才能を自由に使える、神話上のGoogleであるため、単体テスト文化を簡単に採用できたと信じているかもしれません。私を信じてください、「簡単」は私たちの努力を表す言葉ではありません。実際、膨大なリソースと才能は、すべてが可能な限り順調に進んでいるという考えを強化し、そびえ立つ成功によって生み出された長い影の中で問題がくすぶる傾向があるため、邪魔になる可能性があります。Googleは、Googleであるという理由だけで開発文化を変えることができたわけではありません。むしろ、Googleの開発文化を変えたことが、開発環境とソフトウェア製品が引き続き規模を拡大し、増え続ける開発者とユーザーにもかかわらず期待に応え続けるのに役立ちました。
Googleでの単体テストに対する抵抗は、主に、Googleの成長を続ける運用負荷の下で、旧式のツールを使用して新しいコードを書くのに苦労している単体テストに不慣れな開発者の問題でした。既存のコードにテストを追加することは非常に困難に見え、現状を考えると、新しいコードにテストを提供することは無駄に見えました。単体テストに関心のある人々は、単体テストを作成することで、今日書いたコードが正しいという確信だけでなく、6か月後に他の誰か(または元の開発者でさえ)がコードを変更する必要がある場合にも正しい状態を維持するという確信が得られることを、他のGoogle社員に納得させるという困難な作業を行いました。
テスティンググループレットは、単体テストに関心のある私たちにコミュニティを提供しました。テスティンググループレットとその同盟者は、長年にわたって着実に活動し、Google全体にテストの知識を普及させ、新しいツールの開発と採用を推進することに成功しました。これらのツールにより、Googleの開発者はテストをする時間を確保でき、この共有された知識により、時間の経過とともにコードをテストすることが容易になりました。テスティンググループレットのテスト認定プログラムの参加者によって共有された指標と成功事例は、他のチームにも単体テスト/自動テストを試してみるよう説得するのに役立ちました。参加チームは、特定の期間に提出されたコード変更および/または機能の数と、同じ期間におけるバグ、ロールバック、緊急リリースなどの、最も関心のある生産性指標の改善にテスト認定が役立ったと高く評価しました。
テスティンググループレットとは?
「Testing Grouplet」は、Googleのエンジニアたちが、メインプロジェクトとは別に、Google関連のプロジェクトを自由に開発できる時間として提供されていた20%の時間(20%ルール)を使って、Google全体でのユニットテスト導入を促進する課題に取り組んだチームです。資金も直接的な権限もほとんどないボランティアグループであり、Googleの開発者たちにユニットテストの価値を納得させ、それをうまく行うために必要なツールと知識を提供するために、説得とイノベーションに頼っていました。Testing Groupletは、Google全体にユニットテスト文化を浸透させるという壮大な戦略を達成するために、型破りな戦術をうまく活用しました。それらの多くは、以下のサブセクションで説明します。
これらのTesting Grouplet関連の取り組みは、我々の最高のアイデアのいくつかを代表するものであり、たまたま適切なタイミングで適切なアイデアでした。他にも試したものの、うまくいかなかったものはたくさんあります。重要な点は、我々が粘り強く取り組んだことです。新しいアイデアを試し続け、経験から学び、最終的に当時のGoogleの文化の中で特にうまく機能する方法を見つけました。同じ方法が他のチームや他の企業でも有効かもしれませんし、そうでないかもしれません。それでも、他の開発組織で役立つ可能性のあるアイデアのインスピレーションの源となることを願っています。
Testing Groupletは、Google全体で日常的な開発の質と生産性を向上させることを目的とした「Intergrouplet」の集合体の一つに過ぎませんでした。Intergroupletは、すべてのチームに共通する問題の解決を支援しました。Groupletは、草の根からのフィードバック、提唱、その他のサポートを提供することで、公式の専任チームの活動をしばしば補完しました。たとえば、Testing Groupletは、Testing Technologyチーム、Build Toolsチーム、社内トレーニング組織であるEngEDU、そしてエンジニアリング生産性部門全体(後述の「Test Certified」のセクションで説明)と密接な関係を持っていました。開発の質と経験を向上させるための取り組みを拡大した、情熱的なボランティアによって結成された他のGroupletには、Documentation Grouplet、Mentoring Grouplet、Hiring Grouplet、Googleのスタイルガイドと可読性の伝統の守護者であるReadability Grouplet、そして全社的な問題に対処したり、新しいツールを導入したりすることを目的とした「fixits」の伝統を維持していたFixit Groupletがありました。
トイレでのテスト
Testing on the Toilet(TotT)は、Googleのトイレに掲示された1ページの記事シリーズで、Testing Groupletの取り組みと成果の中で最も目に見えるものです。2006年に始まり、毎週のエピソードが公開され続けています。各エピソードは、特定のテスト手法、ツール、または関連する問題の概要を1ページでまとめたもので、世界中のGoogle開発オフィスのトイレに配布されます。下部にある「広告」は、Googleの検索結果広告に似ており、トピックに関連する詳細情報へのリンクを提供します。各エピソードは、すべてボランティアによって執筆、審査、編集、配布されています。長年にわたり、Googleの開発者にユニットテストの利点と適切な適用について教育し、Testing Groupletの取り組みをさらに豊かにした標準的な概念を使用して全社的な会話を開始する上で、非常に効果的でした。これらの会話は、Testing Groupletのメンバー以外の人々が自分のアイデア、議論、経験を提供することで、反響室効果を防ぐのに役立ちました。
なぜ、他の公共スペースではなく、トイレにチラシを貼るのでしょうか?なぜ、メールニュースレターを配信しないのでしょうか?このアイデアは、Testing Groupletのブレインストーミングセッション中に提案されました。アイデアに制限はありませんでした。我々は、社内トレーニング、ゲストスピーカー、書籍の配布など、多くの従来の方法を試してきましたが、人々の注目を集めるための新しい角度を探していました。この特定のアイデアの大胆さと、頭韻を踏んだ名前がグループに響き、それが我々にとって効果的でした。幸いなことに、実際にチラシを貼り始め、活動を始めたところ、このアイデアは定着しました。予想通り、初期には一部の反対意見がありましたが、メディアの価値が明らかになり、テストがアクセス可能なスキルであり、段階的な学習と改善に役立つというメッセージは、シリーズが続くにつれて深く共鳴しました。
テスト認証
Test Certifiedは、Testing Groupletによって設計されたプログラムで、開発チームにユニットテストの実践とコード品質を向上させるための明確な道筋を提供しました。当初は、チームが四半期ごとの目標として採用し、時間をかけて達成できる個別ステップで構成される3つの「レベル」で構成されていました。(最終的には5つのレベルが定義されたと聞いています。)最初のレベルは、ツールとベースライン測定の使用の確立に焦点を当てています(例:継続的インテグレーションサーバー、コードカバレッジ、慢性的に壊れた「不安定な」テストの識別)。2番目のレベルは、すべてのコード変更と新しいコードにテストを要求するテストポリシーを採用および施行し、簡単に達成できるテストカバレッジの目標を設定することに焦点を当てています。3番目のレベルは、チームを高いレベルのテストカバレッジとそれに伴う生産性の向上に導くことに焦点を当てています。
すべてのGoogle開発チームがTest Certifiedレベル3のステータスを達成することが、Testing Grouplet関連のすべての取り組みの最終目標になりました。エンジニアリング生産性部門は、Test Certifiedが、テストエンジニアとソフトウェアエンジニア(テスト)が開発チームとより良くコミュニケーションを取り、全員の時間を有効に活用するためのツールを提供できると考え、プログラムを支持しました。目標は、2010年にTest Automation Platform継続的インテグレーションシステムが展開されたことで事実上達成され、その後、Googleのほぼすべての開発チームがTest Certifiedレベル3で運用されるようになりました。
テスト傭兵
Test Mercenariesは、Googleの開発チームがTest Certifiedステータスを達成するのを支援することに専念したフルタイムのソフトウェア開発者のチームでした。Testing Groupletがチームのコンセプトを提案し、2006年後半から2009年初頭まで存在しました。理想的には、少なくとも2人のMercsが3か月間チームに割り当てられ、その間、Mercsは製品、コード、チームのダイナミクスについて学び、Test Certifiedで設定されたパスに沿って改善されたユニットテストの実践を導入しようとします。チームごとの成功は、生産性への影響という点で変動があり測定が困難でしたが、Test Mercenariesの集中的なフルタイムの取り組みは、他のすべてのボランティアベースのTesting Groupletの取り組みを大幅に強化しました。Test Mercenaryの経験は、多くのTest Certifiedの議論とTesting on the Toiletのエピソードに情報を提供しただけでなく、文化全体にユニットテストの導入を促進する上で重要なツール開発にもつながりました。
テストフィックスイット
Fixitsは、重要であるにもかかわらず、ほとんど棚上げされていた問題にGoogleの開発コミュニティ全体を集中させるために組織された短いイベントでした。また、新しいツールの展開や、開発者が遭遇した可能性のある問題への対処にも役立ちました。Fixitsは通常、1日から1週間続き、各イベントに投入された計画と参加者のクリティカルマスのおかげで、いくつかのGroupletや他のチームが大きな変化を起こすために使用した最も効果的な手法の1つでした。
Testing Groupletは、2006年8月と2007年3月に、壊れたテストの修正と、未カバーのコードの新しいテストの作成に焦点を当てたTesting Fixitsを開催しました。また、2008年1月にはRevolution Fixitを開催し、Build Toolsチームの強力な新ツールを導入して、開発とテストの速度を劇的に向上させました。2008年の夏に数か月間続いたTest Certified Challengeでは、多くの新しいプロジェクトを採用し、他の多くのプロジェクトがより高いTest Certifiedレベルに移行するのを支援しました。Build Toolsチームの2009年10月のForgeability Fixitでは、ほぼすべてのビルドターゲットとテストがクラウドでビルドおよび実行されるようになり、Testing Fixit/Testing Groupletアーク全体の集大成である2010年3月のTAP Fixitに向けて完璧な準備が整いました。TAP Fixitでは、Google全体にTest Automation Platformが導入されました。
これらの目標指向のイベントは、Testing Groupletが開始した他の長期的な取り組みを強調し、全体的なユニットテストの導入ミッションを次のレベルに引き上げました。新しいfixitごとに、以前のfixitの経験と勢いを活用しました。Testing on the Toiletは、これらのイベントに関する情報を広め、事前にGoogleの開発コミュニティを準備する上で非常に貴重なツールであることが証明されました。
fixitの実行に際して、役員からの許可や指示は必要ありませんでした。グループが実行を決定したら、実行しました。(ただし、エンジニアリング担当副社長は、通常、参加を奨励する準備された発表を送ることに同意しました。)Fixit Groupletは、fixitチーム間の調整を支援し、最適な日付を選択するようにしました(例:9月初旬のBurning Man weekには、Mountain Viewの半分がプラヤにいるため、fixitを避けるなど)。また、互いの取り組みを食い合わせ、「fixit疲労」として知られる状態にならないようにしました。Fixit Groupletはまた、新しいfixitが過去のfixitの経験から恩恵を受けられるように、ツール、ドキュメント、歴史、アドバイスを提供しました。
スタイルガイド/コーディング標準
すべてのGoogle開発者は、日常的に使用する各言語で「可読性を獲得する」必要がありました。「可読性を獲得する」とは、開発者が言語固有のスタイルガイドの多くを内面化するガイド付きのプロセスでした。「可読性を獲得する」にはコードの記述が含まれていましたが、最終的な目的は、会社全体の規則に従って、記述したコードが他の開発者にとって「可読」であることを保証することでした。控えめな存在のReadability Groupletは、この非常に貴重なプロセスを維持したボランティアチームでした。ソース管理メカニズムにより、可読性ステータスを獲得せずに、特定の言語で長期的にコードを作成することは非常に困難になりました。これにより、スタイルガイドが適切であり、広く適用されることが保証されました。
エラーを回避するためのスタイルガイドの例として(中括弧、スペース、名前に関する無意味な議論を避けるのではなく)、現在のGoogle C++スタイルガイドでは、ヒープ割り当てされた関数パラメータは、呼び出し先が所有権を仮定する場合はstd::unique_ptr
を介して渡さなければならず、呼び出し元が所有権を保持する場合はconst参照で渡さなければならないと規定しています。これはC++ではメモリが自動的に管理されないため必要であり、開発者が視覚的に不適切なメモリ管理を認識できるように訓練することは、静的および動的解析ツールがそのようなエラーを検出するのを待つよりも価値があるからです。(Googleは同様のツールも実行していましたが、コストがかかり、フィードバックサイクルが長くなっていました。)
Googleのソースコードリポジトリのほぼすべてが、すべての開発者が閲覧し、個人用の作業コピーにチェックアウトすることができました。Googleのスタイルガイドは特定の言語のすべてのプロジェクトに適用され、命名規則の多くは言語ガイド間で類似していたため、Googleの開発者はこれまで見たことのないコードベースの部分を簡単にスキャンして、比較的迅速に理解することができました。これにより、Googleの従業員はさまざまなプロジェクトに貢献し、繰り返されるコードをすべてのプロジェクトで再利用可能な共通ライブラリに抽出し、他のプロジェクトのバグを特定して修正し、新しいコーディングスタイルに適応するという摩擦に耐えることなくプロジェクトを切り替えることが容易になりました。
コードレビュー
Googleは設立当初からコードレビューの実践を導入していました。コードは、作成者以外の誰かによってレビューされ、明示的に承認されるまで、ソース管理にコミットされませんでした。プロジェクトの「オーナー」が関連するレビューに含まれることを保証するためのコントロールが存在していました。コードのレビューは、コードを書くことと同じくらいプログラマーの日々の責任であり、時にはそれ以上でした。共通のスタイルガイドはプロセスから多くの摩擦を取り除き、レビュー担当者はスタイルが間違っていると思われる箇所を迅速に指摘し、変更そのものの意味に可能な限り集中することができました。内部ツールは、開発者が入ってくるレビューと出ていくレビューのキューを管理するのに役立ち、すべての開発者がすべてのコード変更のステータスと議論を把握できるようにしました。
Test Certified Level Twoの要件のおかげで、ほぼすべてのチームは、すべてのコード変更にテストを添付するという正式な書面による開発ポリシーを持っていました(すでにカバーされているコードの既存の動作を変更しない純粋なリファクタリングを除く)。最終的に、ビルドツールとテスト技術チームは、テスト結果(またはその欠如)をコードレビューツールに直接統合しました。レビュー担当者は、著者がテストを実行し、特に以前のレビューコメントに応じて変更が加えられた場合に、テストが合格していることを確認したかどうかを確認できました。
低レベルの詳細を隠すための共通インフラストラクチャ
大規模な共有ソースリポジトリと全体に適用される統一された言語スタイルを考慮して、GoogleはGoogleのすべてのプロジェクトで再利用された低レベルの詳細を隠すための共通ライブラリの開発を奨励しました。最も普及した例は、リモートプロシージャコール(RPC)およびプロトコルバッファのインフラストラクチャでした。プロトコルバッファは、RPCシステム内および階層的で、多くの場合シリアル化されたデータ構造が必要な他の多くの場所で使用されるデータ記述言語です。Googleの誰かがシリアル化された構造を定義し、メモリバッファを直接操作しようとした場合(Heartbleedバグを含むコードでのバッファ操作など)、コードレビュー担当者が最初に言うことは、「なぜprotobufを使わないのか?」でした。
この共通インフラストラクチャはすべて広範囲にユニットテストされており、RPCインタラクションのシミュレーションやprotobuf値の初期化/比較を簡単にするユニットテストインフラストラクチャが存在していました。
テスト自動化プラットフォーム継続的インテグレーションサービス
2005年にTesting Groupletが最初に開始されたとき、既存の中央集中型テストサービスであるユニットテストフレームワークは、需要に対応できませんでした。これは、会社内のすべてのテストを構築および実行し、結果をデータベースに格納するために専用のマシンセットを使用していました。ただし、システムへの負荷が増加したため、フィードバックサイクルはますます長くなり、その価値が低下しました。
これに対応して、2人の広告開発者が、独自のシングルマシン、プロジェクト固有の継続的インテグレーションフレームワークを開発しました。これは「Chris/Jay Continuous Build」として知られています。このフレームワークは、Test Certified Level Oneの要件として含まれていることもあり、Google全体に普及しました。これは、Googleプロジェクトに比較的柔軟な継続的インテグレーションサーバーを提供し、Testing GroupletのTest Certifiedミッションを長年にわたって十分にサポートしていましたが、C/Jビルドを使用する各チームはかなりのメンテナンスが必要でした。
2008年1月のRevolution Fixitの結果として、テスト自動化プラットフォーム(TAP)がGoogleの中央集中型継続的インテグレーションシステムになりました。2010年3月のTAP Fixit中にGoogle全体で展開されたTAPは、クラウドインフラストラクチャを利用してビルドアクションとテスト実行を大規模に並列化するGoogleの社内ツールチェーンに基づいて構築されました。TAPは、すべてのコード変更によって影響を受ける会社全体のコードベースのすべてのテストと、特定の変更によって影響を受けるテストのみを、数分以内に実行しました。(Googleが私が去ってから成長を続けているため、この時間スケールは今では変化している可能性があります。)TAPビルドは、1つの短いWebフォームで構成され、どのプロジェクトも複数のビルドを持つことができました。TAPのデータ収集コンポーネントであるSpongeは、自動ビルドまたは個々の開発者によって実行されたかどうかにかかわらず、すべてのビルド試行とテスト実行の結果を収集し、ビルドコマンドと完全な実行環境を記録し、後で検査するための情報をアーカイブしました。TAP UIは、会社内のすべてのプロジェクトに影響を与えるすべての変更を簡単に可視化しました。
TAPは、Testing Groupletの取り組みの究極の成果を表していました。テストテクノロジーチームがビルドツールチームと緊密に協力して開発したTAPは、長年の努力の末、頂上まで岩を押し上げました。私がGoogleを去るまでに、ほぼすべてのチームが少なくとも1つのTAPビルドを持っており、ほとんどのビルドの破損は、ほとんどのビルドコップがそもそも破損に気づく前にロールバックまたは修正されました。
TAPはレベル11へ
もし最後のセクションがまだ理解されていない場合は、もう一度言います。集中管理された継続的インテグレーションインフラストラクチャ。1ページのワンクリックでビルドプロジェクトをセットアップ。会社内のすべての変更は、クラウドでの分散ビルドと実行を介して、数分以内(少なくとも私がそこにいたときは)に統合、構築、およびテストされました。すべての結果が保存され、社内のすべての開発者が可視化されました。ほとんどの破損は、ほとんどの関連プロジェクトが気づく前に修正されました。天国、涅槃、ヴァルハラ、ストーンヘンジ—あなたがそれを呼びたいものは何でも、TAPがそれでした。
ビルドモニタリングオーブ
Googleでの私の最初のコーディングプロジェクトは、光る球体—片手でバランスが取れるほど小さく、立方体の壁または棚に置くとチーム全体に見えるほど大きい球形のランプ—の色とパルスを、Chris/Jayの継続的ビルドの合格/不合格ステータスに基づいて変更するスクリプトを書くことでした。時間が経つにつれて、このスクリプトは、さまざまな継続的インテグレーションシステム(最終的にはTAPを含む)で実行されているビルドプロジェクトのめまぐるしい組み合わせを処理し、NYC風の自由のロバーティ像(はい、トーチは異なる色で光ります)を含むいくつかの異なるハードウェアオーブデバイスを制御するようにスコープを拡大しました。最終的にブラウザプラグインは、デスクトップにいてもラップトップを使用してログインしているかどうかにかかわらず、個々のチームメンバーにとってより目に見えるリマインダーとして機能しましたが、共有チームスペースの物理的なオーブは決して完全に時代遅れになることはありませんでした。
オーブの目的は3つありました。1つは、ハッキングするのが楽しかったことです。すぐに具体的な方法でテスト文化を推進したい人のために、オーブプロジェクトをまとめたり拡張したりすることは、それを行うための楽しい方法でした。これは、人々をTesting Groupletプロジェクトに採用し、エネルギーと進歩の感覚を生み出し、士気を高めるのに役立ちました。もう1つは、Testing Groupletが、テスト認定プログラムにサインアップしたチームへの「賞品」として、ニフティなノベルティで報酬を与えることで行動を促すという、古くから続くGoogleの伝統の中でそれらを使用したことです。私たちはGoogleの性質に沿って進み、それに逆らわなかったのです。資金と権限がなかったため、Testing Groupletは利用可能なリソースと文化的力を最大限に活用して変化をもたらす必要がありました。実際、これらの制約が、お金や権限がどれほどあっても得られないような、より永続的な創造的なソリューションを生み出すことを強制したと私は主張します。
最後に、物理的なビルドオーブは、完全なコミュニティダッシュボードに次ぐ優れた情報ラジエーターです。おそらく、オーブは完全なダッシュボードを備えたチームでも、遊び心のある「恥をかかせる」文化を奨励するため、まだ役割があるかもしれません。これにより、チームメンバーはオーブの健康状態を個人的に心配し、ビルドの破損のためにオーブが不機嫌そうに見えるときは、互いに責任を負わせます。
新入社員の洗脳
Googleの社内トレーニング組織であるEngEDUと協力して、Testing Groupletは入門ユニットテストの講義と実習を作成しました。これにより、Googleに入社するすべての新しい開発者が、少なくとも利用可能なツールとフレームワーク、ユニットテストの背後にある理由、およびいくつかの基本的なユニットテストの原則とテクニックを認識していることが保証されました。通常、Testing Groupletのメンバーによる1時間の講義の後、Nooglerは別のTesting Groupletメンバーが監督する実習に参加し、学んだばかりのことをすぐに実践しました。Testing Groupletは、この実習で使用される内部資料の作成と維持を支援しました。
Testing on the Toiletが開始された後、Nooglerは会社が成長し、より多くのオフィススペースを取得するにつれて、マウンテンビュー全体での配布を改善するための主要なメカニズムになりました。私たちは、その週のTotTエピソードを自分の建物に投稿することを志願した勇敢なNooglerに本またはTシャツを約束して、ユニットテストの講義を終えました。私たちは彼らを「Noogler Army」と呼びました。これは、人々をユニットテスト文化に参加させ、楽しんで、大義に所属し、早期に貢献しているという感覚を持ってもらうための別の方法でした。
そしてもっと…
Googleには、可能な限り最高のコード品質を保証し、壊滅的で予防可能な欠陥を回避するための他のツール、プロセス、およびテストとステージングのレイヤーがありました。それらはすべての欠陥を捉えたわけではありませんが、通過した多くの欠陥は比較的小さく、迅速に特定して修復することが容易で、負の副作用を恐れる必要はありませんでした。より困難な欠陥も、通常はより高いレベルの自信とスピードで対処できました。高いレベルのユニットテストカバレッジを含む自動テストは、開発業務とユーザーベースの大規模さにもかかわらず、高い生産性を可能にするこの恐れのない環境にとって不可欠でした。
しかし、Googleが素晴らしくて何でも正しく行っている一方で、あなたのチームや会社はどうしようもなくダメだという印象を与えたくはありません。私がこの説明をしたのは、アイデアを育むためであり、あなたの環境が理想からどれほど遠いかを思い知らせるためではありません。信じてください。私が説明している、私が去ったGoogleの環境は、私が最初に入社した頃のGoogleとは全く対照的でした。私のテスティンググループの仲間たちと私は、資金も足りず、圧倒的に人数が不足していました。文化を変えるという私たちのコミットメントを実現するためには、小さなところから始め、何年もかけて地道に努力する必要がありました。
重要なのは、最終的には、私たちに不利な状況にもかかわらず、それを実現できたということです。この記事を締めくくるにあたり、私がGoogleでの経験から得た、あなたのチームや会社全体で同様の変化を長期的に実現するためのより明確な洞察を提供できるかもしれない、いくつかの一般原則を説明したいと思います。
文化を変える方法
あなたは、"goto fail"やHeartbleedはユニットテストによって防げたはずだと確信しているかもしれません。コードをオープンソース化することで、ユニットテストの必要性が減るのではなく、増すべきだと確信しているかもしれません。ユニットテストは欠陥防止に加えて多くのメリットを生み出し、コストをかける価値があると思っているかもしれません。この記事の概念実証テストを試したり、自分のコードをテストし始めたりした後で、それを実感しているかもしれません。ユニットテストが既存のツールやプラクティスの適用を改善するために役立ち、Googleが企業全体でユニットテストを推進している例に刺激を受けているかもしれません。
さあ、あなた自身のプロジェクト、チーム、または会社で変化を起こし始める準備ができました...しかし、どこから始めればよいのか見当もつかないかもしれません。ここでは、あなたを導くのに役立つかもしれない個人的な洞察をいくつか提供します。これは文字通りに従うべき処方箋ではなく、結果を保証するものでもありません。しかし、これらが、あなたの環境全体でユニットテストの導入を推進する上で役立つ独自の洞察を育むのに役立つことを願っています。
あなたが望む変化になれ
(引用:マハトマ・ガンジー)
気づいているかどうかは別として、あなたはすでに始めています。この記事を読み、その主張を理解しました。あなた自身でユニットテストの経験を内面化しました。これにより、ソフトウェア開発に関するあらゆる議論において、基礎となる土台と視点を得ました。たとえ誰もあなたについてこなくても、今すぐ行動に移すのを妨げるものは何もありません。まだ誰かの考えを直接変えようとしないでください。自分のコードのテストを書くことで、それがどのように行われるかを示すだけです。以下の参考資料のセクションにあるような、ブログ、雑誌、書籍、セミナーを探して、あなたのスキルを磨いてください。Martinのウェブサイトにあるすべてに目を通してください。Meetupに参加してください。たとえば、ボストン、ニューヨーク、サンフランシスコ、フィラデルフィアのAutoTest Meetupsに参加するか、自分自身で開始してください。率先垂範し、道を外れないようにしてください。
既存のコードで小さく始める
"goto fail"とHeartbleedの概念実証の例、Google Web Serverのストーリー、そしてGoogleの全体的なストーリーが示すように、既存のコードを今すぐに改善し始めることができます。コードベースが改善される唯一の方法は、それに取り組むことであり、議論や議論は、実際にテストを書くほど効果的ではありません。模範を示すことで、他の人が従うべきパターンを提供することで、これらのアイデアがあなたのチームのコードでも機能することを実証しており、機能するコードはそれ自体が最良の議論です。
既存のコードベースの一部を取り出して、それに対するテストを作成します。必要に応じてコードをリファクタリングします。テストに適した、独立したユニットとして機能する関数やクラスを抽出します。既存のコードに新しい機能を追加するときは、それが十分にテストされたユニットの一部であることを確認し、必要に応じて新しいユニットを使用してコードをリファクタリングします。
可能であればユニットテストフレームワークを追加します。それ以外の場合は、この記事で提供されている例を研究して、フレームワークなしでどのように乗り切るかを学びます。問題に少しずつ取り組みます。やがて、あなた一人でどれほどのことを達成できたかに驚くでしょう。
小/中/大のテストピラミッド
ユニットテストは、コードまたは製品の品質に対する万能のソリューションではありません。それを約束してはいけません。テスティンググループは、Small/Medium/Largeテストサイズスキーマの概念を開拓しました。Mike Cohnのテストピラミッドは、それと驚くほど似ています。ユニットテストが果たす根本的な役割を全員が明確に理解していることを確認しますが、過度に売り込んではいけません。
継続的インテグレーションをセットアップする
継続的インテグレーション環境を構築するために、できることは何でもしてください。懇願したり、借りたり、盗んだりする必要がある場合でもです。必要であれば、シェルスクリプトとcron
ジョブを使用して独自にロールアップしてください。たとえそれがあなたのワークステーションで実行されるとしてもです。最初はテストを実行しなくても、コードがビルドできる(コンパイル言語の場合)ことと、プログラムがいつでも起動できることを保証できることは、ユニットテスト文化を広めるための重要な前提条件です。コードがそもそもコンパイルできない場合、ユニットテストはほとんど役に立ちません。
あなたのチームが、コードが常にコンパイル可能な状態であることを保証するという習慣をまだ持っていない場合、それはユニットテストの導入を推進する前にあなたが勝つ必要のある最初の戦いかもしれません。全員が完全に別々のブランチで開発し、統合が後になってから行われる場合は、統合作業を秘密裏に行ってください。これらの異なるブランチからプルして統合するための独自のgitリポジトリを設定します。人々があなたの行動と、あなたがどれほどの頭痛を回避するのに役立っているかを知れば、あなたは十分に役立つ信頼を得るでしょう。
可視性を最大化する
他の人がビルドが壊れたときに確認できるようにします。かつては継続的なビルドやテストに無関心または敵対的だった人々や管理者は、デスクの見やすい場所に設置された監視デバイスによって考えを変えてきました。これは、ビルドが壊れたときに人々が自然に質問(「なぜまたそれが赤くなっているの?」)を始めるからであり、時間の経過とともに、それは全員の態度に大きな影響を与える可能性があります。私たちが見ることができる問題だけを気にするのが人間の性なので、問題が発生した場合に人々が簡単に確認できるようにします。
監視デバイスは、個人のブラウザのプラグイン、中央に配置された光る球、ビルドダッシュボードを表示する大きなモニタースクリーン、特別に配線された信号機など、さまざまな形にすることができます。人々がビルドの現在の状態を認識しないようにするには、意図的に努力する必要があるくらい目立つようにする必要があります。
可視化ツールは、楽しさを加えることもできます。チームは、テストステータスの表示方法で想像力を働かせ、面白く競争することができます。Googleの1つのチームには、ビルドが壊れると騒々しく動き出す羽ばたくペンギンがいました。もちろん、周囲のすべてのチームは、それと同じくらい良いものを見つけようとしなければなりませんでした。それはすべて、メッセージを広めるのに役立ちます。
共犯者
最終的には、説得する必要のない仲間、犯罪の相棒と力を合わせる必要があります。あなたたちは互いのアイデアに挑戦し、強化し、抵抗に直面して立ち向かう時が来たら、互いに道徳的なサポートを提供します。互いに意見を交換することで、議論、方法、慣用句などを開発します。潜在的な批判者よりもこれらのアイデアを批判的に検討しますが、互いに礼儀正しく敬意を払って接してください。互いをより良くし、最終的にはチームや会社全体をより良くすることができるかもしれません。
(あらゆる分野で)人々のグループを説得しようとするとき、すでにあなたに同意している人に最も近い人から始めるのが常に最も簡単です。あなたが他の1人に同意を得ると、あなたはもはや孤独ではなく、誰も信じていない奇妙なアイデアを持つクレイジーな男ではなくなり、今ではあなたたち2人が説得をしています。3人目、そして4人目を得ると、勢いがつきます。
他の人を巻き込むためのもう1つの微妙で効果的な方法は、アドバイスを求めることです。あなたのチームの誰かがテストに抵抗している場合、あるいは単にそれに不慣れな場合は、その人にあなたのコードとテストを確認するように依頼してください。あなたが思いつかなかった他のテストがあるかどうかを尋ねてください。ほとんどのプログラマーは喜んで意見を提供し、それは彼らにテストを強制することなくテストに関与させる方法です。時間の経過とともに、彼らは自分の意思でユニットテストを提唱するようになるまで確信するかもしれません。
教育する
チーム全体に知識を広める方法を見つけてください。毎週のランチミーティングのような簡単なものや、バスルームに毎週チラシを投稿するようなクレイジーなものにすることもできます。チームに話をするように人を招待するか、講演会やMeetupに行くためにチーム外出を組織してください。アイデアやツールを共有および議論するための内部メーリングリストを開始してください。
委任せよ、委任せよ、委任せよ!
逆説的ですが、物事を実現するために直接行うことが少なければ少ないほど、より多くのことを実現できます。ビジョンと方向性を確立できれば、特定の役割を引き受け、それらを積極的に実行してくれるボランティアを見つけるでしょう。これにより、あなたが構築しているコミュニティ内で彼らに所属感と価値観を与え、あなたがより大きな全体像に集中することができます。
いくつかのFixitsを実行した後、私はすべての責任を自分自身で抱え込むよりも、人々が満たす必要のある役割の明確なリストを作成する方がはるかに生産的であることに気づきました。それ以降、役割のリストを事前に提示することで、草の根組織を非常に迅速に立ち上げることに非常に効果がありました。チームまたは組織で今すぐに検討できるいくつかの役割(そして、名前の一部は、軽く楽しいものにするために意図的に滑稽です)を次に示します。
- 歴史家:注目すべき問題や活動、およびその成果物を、一元的にアクセス可能なリポジトリ(例:wikiまたはチームブログ)に文書化、要約、アーカイブします
- 情報大臣:講演、ブログ投稿、記事などを制作するように人々に個人的に依頼します。次に、この人は講演者、著者、ボランティア編集者のサブコミュニティ(Testing on the Toiletのような)を率い、おそらくコミュニティ固有の知識ベースを育成することもできます(例:wikiを使用)。
- 宣伝大臣:チームの活動に関するアナウンスを、メール、チラシ、目立つ壁の投影、著名な管理者、役員、その他の代表者に与えられたスクリプトなど、さまざまなメディアを通じて監督します。
- コミュニケーション大臣:チームが利用できるコミュニケーションチャネルの健全性を監視し、改善を提案および実装します(情報大臣と協力して)。おそらく、連絡先情報と成果物のアーカイブのリストを管理します(歴史家と協力して)。
- 言葉の職人:新しい成果物の保守と整理を専門的に処理する人。たとえば、投稿にタグが付けられていることを確認し、CSSスタイルを試したり、コンテンツが検索エンジンで簡単に見つけられるようにSEO作業を行ったりします(成果物が公開されている場合など)。
- スケジューラー:ロジスティクス(例:誰がいつ話すか、イベントがどこで開催されるか)を追跡します。適切な会場のリストを維持し、新しい会場を探します。
- フェストマイスター:イベントのために、ビールとピザ、そして配布されるすべてのお菓子がすべて準備されていることを確認します。
- ハートアンドソウル:講演者、著者、その他の貢献者やゲストにフォローアップし、個人的なメール、ギフト券、グッズ、小規模なパーティーなど、さまざまな形でチームを代表して感謝の意を表します。
これらは私が思いついたほんの一例ですが、注目してほしいことがあります。今、あなたは、気づいているかどうかにかかわらず、これらの役割すべてを満たしているかもしれません。一人で行うには大変な作業であり、あなたを疲弊させ、チームを本当の意味でのコミュニティに成長させるための重要な機会を逃しています。
セイウチになれ
では、すべてを委任した後、あなたに残される役割は何でしょうか?私は自分自身を「セイウチ」と呼びました。これは私がビートルズの熱狂的なファンだからですが、役割の本質は「オーガナイザー」です。あなたは、専門家チームを管理する全体像を見据える人です。あなたは、方向性と優先順位を設定し、重要な責任を負わせた創造的な人々にフィードバックを提供し、彼らが遭遇する障害を取り除くという特権を持ち、人々がタスクにもたらすエネルギーと創造性に常に驚かされ、あなたが想像さえしなかったような素晴らしいことを成し遂げることができます。
チームワークの力を受け入れよ
他のすべての役割にしがみついていると、オーガナイザーとして成功する能力が妨げられ、それはコミュニティの潜在能力を最大限に引き出すことを妨げます。したがって、コミュニティのためにすでにしていることのリストを作成し、それらを一連の役割に体系化し、各役割に最も適していると思われる個人を積極的に関与させることをお勧めします。
私は、自分の名前が太字の赤字で書かれた役割名のリストを作成し、「この事業の成功は、赤字で自分の名前がまだ付いている役割の数に反比例する」と皆に言うこともありました。(緑色で自分の名前が付いていたのは「セイウチ」だけでした。)そのようなリストと、明確に定義された役割を提示されると、驚くほど早く人々は自発的に行動します。
とは言え、そのような役割を担う人々は、すべての決定をあなたを通して行うことなく相互に交流することが奨励されるべきです。役割は責任を明確にするのに役立つため、あなたは細部にまで関与する必要がなくなり、人々は多くのことを自分たちで解決できます。誰もが良いアイデアを探し、良いアイデアを発展させ、それらを共有することを奨励されるべきです。あなたは常に状況を把握しているべきですが、人々はあなたが耳を傾けていると感じるべきであり、彼らを監視したり、上司になろうとしているようには感じさせるべきではありません。彼らがあなたを喜ばせるような驚きを与えてくれることを期待してください、そうすれば彼らはそうしてくれるでしょう。
自分を時代遅れにせよ
初日から後任を探し始めてください。あなたの異動後に崩壊するような脆弱な事業はあってはなりません。それは人生全般にも当てはまります。単体テストの文化を広めることに関しては、「テスト担当者」や「テスト担当の女性」という肩書きに固執したくはないでしょう。あなたが離れる必要や離れたいときに、人々が積極的にステップアップしてくれるようにする必要があります。それがレガシーを築く方法です。
フィックスイットを実行する
役割と修正作業といえば、単体テストを推進するために構築しているコミュニティを団結させるための楽しく生産的な方法は、fixitを実行することです。小さなチーム規模のfixitから始め、後でオフィス全体、さらには会社全体のイベントを実行できます。始めるのに必要なのは、明確な目標(例:すべての壊れたコード/テストを修正する、カバレッジをX%増やす、素晴らしい新しいツールを採用する)、明確に定義されたボランティアの役割(上記のように)、および実行する必要のあるタスクと各タスクを担当する人を追跡するための共有スプレッドシートのようなものです。次に、日を選び、周知し、それを実現してください!チームの協調的な努力を適用して士気を高め、厄介で長引く問題を解決することに勝るものはありません。
fixitが楽しく生産的であるだけでなく、なぜその目的のために不可欠であるかを例を挙げると、大規模なプロジェクトがコンパイルすらできない状態にあるケースを考えてみましょう。これは継続的インテグレーションの設定を完全に妨げ、人々が変更をコミットする前にすべてをテストしようとするのではなく、テストするプロジェクトのブランチを選択し始めることを推奨します。言い換えれば、彼らは<ツール> test subprojectyBitA/**/* anotherBitB/ohAndThis/**/* partC/**/* ...
を実行し、<ツール> build **/*
を実行したり、テストサイズなどの他のより適切な選択基準でテストしたりしません。その結果、慢性的な問題は悪化する可能性があり、継続的インテグレーションは手の届かないところに残ります。
このケースはfixitに最適です。コードの破損箇所を事前に特定してスプレッドシートにまとめることができます。次に、重複した作業を避けるために、特定の破損箇所を処理するボランティアを募ることができます。チームは1つの専用スプリントでこれらの問題に取り組むことができ、イベントを楽しくお祭りのようにすることができます。そして、コードは最終的に継続的インテグレーションとテストに適した状態になるでしょう。すべてがすぐに修正されなかったとしても、チームは慢性的な問題を解決するために得られた具体的な進歩に励まされるべきであり、最終的に問題を完全に解決する動機となる洞察を得るはずです。
自分自身を信じよ
開発文化を変えることは形式的なことではありません。適切なデータをプラグインするだけで目的の結果が得られるわけではありません。おそらくいつか誰かが正式な学術研究を行い、単体テストの有効性について合理的な主張を裏付ける普遍的に合意されたメトリクスを収集するでしょう。しかし、それさえも人々が耳を傾けて行動を変える保証はありません。たとえ話として、医師が感染症予防のために手を洗うことの有効性を確認する科学的研究は、1世紀以上前から収集されており、研究は今日まで続いています。この重要な証拠にもかかわらず、患者のために手を洗うようにまだ注意する必要がある医師もいます。
業界の歴史のこの時点では正式な研究が不足しているにもかかわらず、(優れた)単体テストの経験によってもたらされる長期的なメリットは、何度も再現されてきた観察可能な現象です。多くのチームが、品質と生産性の向上を反映していると考えられる欠陥率やその他の要因に関するデータを収集しています。このようなアイデアについては、以下の測定、実施、努力のサブセクションで検討されています。単体テストの経験を積んだら(特に別のチームや別の会社で成功を収めたことがある場合は)、「私の経験では」という議論に頼ることは何も悪いことではありません。あなたの経験は、あなたのチームや会社があなたを雇った理由の大きな部分ではないですか?表向きには、彼らはその経験をある程度評価しているはずです。それを過小評価しないでください。「権威主義を避けよ」と助言しますが、あなたは「私がそう言うから」と言っているのではなく、「私がそうしてきたから」と言っているのであり、あなたの具体的な努力と成果を示すことができます。大きな違いです。
これは、経験からの立場で議論することは、あなたが示すべき実際の経験を持っている限り、ごまかしではないからです。たとえば、「goto fail」やHeartbleedが単体テストで検出できた可能性があると言うのはごまかしかもしれません。動作する概念実証コードを作成するのはそうではありません。「コードはテストするのが難しすぎる」と言うのはごまかしです。Google Web Serverが規律ある単体テストの実践のおかげでひどくガタガタな状態からきちんと整理された状態になったため、そうである必要はないと言うのは違います。
良心とある程度の謙虚さを持ち、単体テストや自動テストの利点を誇張しないように注意することは素晴らしいことですが、自分自身を信頼することを忘れないでください。良心と謙虚さは、最終的にはデータではなく経験の関数である自信なしでは役に立ちません。
焦点を維持する
単体テストの採用を推進するためであれ、他の何らかの結果を達成するためであれ、文化変革の最終目標は、誰もが何らかの形で生活を向上させることです。単体テストは、欠陥の減少、生産性の向上、ビジネスの成功、そしてそれらすべてが開発者とユーザーの幸福につながることが期待できるという目的を達成するための手段です。技術的、戦術的、または戦略的な議論に深く焦点を当てているとき、これを見失いがちです。そのようなことを議論することは重要ですが、単体テストとその長期的なメリットとの関連性を明確に保つことも、少なくとも同じくらい重要です。
そのため、可能な限り、取り組みに関わるすべての人々、そして影響を与えようとしている人々にも必ず確認するようにしてください。コーヒーやランチの時、チームミーティングやコードレビューでの何気ないコメントなど、形式ばらない形でも構いません。フィードバックを求め、軌道修正するあらゆる機会を捉えることを習慣にしましょう。この習慣を養うことで、ユニットテストの問題や機会についてより深く考えるように促し、時間とともに皆の考え方を大きな変化へとゆっくりと開いていくでしょう。
指先の感覚を養う
Fingerspitzengefühl(フィンガースピッツェンゲフィール)は、軍事用語に由来する、極めて高い状況認識を意味する言葉です。誰が、どこで、いつ、何に取り組んでいるかを把握する方法を学びましょう。すべてを自分で行う必要はありませんが、チームやコミュニティが行う活動をより良く指示できるように、何が起こっているかを知る必要があります。他者にも同じ感覚を養うよう促し、機会に対して常にオープンで敏感であり続け、適切なタイミングでそれらを掴むようにしましょう。
戦略を浮上させる
最初から壮大な戦略を心に描くことは重要ではありませんが、戦略が現れたときに、それに行動を起こせるような体制をコミュニティ内に構築するように努めましょう。実際、最初に取り組むべき最良の戦略は、コミュニティがいつか何を達成するかを心配するのではなく、コミュニティを構築することに焦点を当てることだと私は考えます。適切な戦略が現れ、人々がフィンガーチップフィール(指先感覚)を養ったとき、コミュニティはそれを実行するために何をすべきかを自然に理解するでしょう。(私の参考例は、もちろん、Testing Groupletが、大規模で統一的なTest Certified戦略が登場するまでに、さまざまなアイデアを数年間検討していたことです。)
とはいえ、有望な焦点を当てる領域を探すのは良いことです。目立つソフトウェアのバグや開発プロジェクトの失敗は素晴らしい例です。「goto fail」やHeartbleedのときに行ったように、ユニットテストの価値をできる限り具体的かつ詳細に説明するためにそれらを活用しましょう。事例を積み重ね、コミュニティのそのような機会に対する感受性を高めましょう。コミュニティメンバーにブログ記事や講演を制作するよう促し、地元のMeetup、さまざまな企業や会議で講演をしてみるのも良いでしょう。(結局のところ、Meetupとは、非常にローカライズされた定期的な会議ではないでしょうか?)有望な講演者や著者を見つけ、彼らがさらに活動するように優しく促しましょう。
この特定のアングルがあなたに魅力的でない場合は、他に適切な、有望な糸口を見つけ、どこまで進めるか試してみてください。
メンターを見つける
これらすべてをまとめようとする際に、信頼して相談できるメンターを持ち、助言を求めることは非常に役立ちます。しっかりとした指導をしてくれそうな人には誰でも尋ねてみてください。彼らは興味がないか、時間がないかもしれませんが、尋ねることに費用も痛みもありません。多くの場合、人々はとても喜んで受け入れてくれます。または、彼らができない場合は、他の誰かを紹介して、紹介を取り持ってくれるかもしれません。
いずれにせよ、アンテナを張りましょう。これをすべて自分だけでやろうとしないでください。
自然と協力し、自然に逆らわない
聴衆を十分に意識しましょう。人はそれぞれ異なる説得の形に反応します。まったく説得されない人もおり、歴史によって引きずられるしかありません。最も深く共鳴する洞察と経験を提供することで、一人ずつ影響を与えてください。よくある言い訳がある場合は、根本的な問題を解決してください。「テストする時間がない」という場合は、ツールをより高速化してください。「自分のコードはテストするのが難しすぎる」という場合は、テストを容易にする方法に関する情報と例を提供してください。トリッキーなコードのテストを作成するために、誰かと協力することを申し出ましょう。説得は常に強制よりも望ましいですが、どちらの機会も探して、チャンスがあればつかみましょう。
一度に一つのチームずつ
Testing Groupletの目標は、Google全体を変えることでしたが、その目標は最終的には一度に1つのチームずつ達成されました。Test Certifiedプログラムは、単一チームレベルでユニットテストの実践とカバレッジを改善するための段階的な計画を提供し、各チームが質問に答え、進捗を遂げるのに役立つメンターを提供しました。トップダウンで会社全体を変えようとすると、失敗に終わる可能性が高く、たとえ短期的には成功したように見えても、長続きする可能性は低いでしょう。
測定、強制、努力
どのような開発アプローチでも、その有効性を測定することは課題の1つです。疑念を抱く人は、テストへの投資がコストに見合うことを「証明」するよう求めてくるかもしれません。他の開発選択(どの言語、フレームワーク、IDEを使用するかなど)と同様に、これを明確に測定することを困難にする多くの要因があります。代わりに、チームが現在の目標と問題に関して意味があると考えるものを測定してください。完全なメトリクスを達成することは不可能かもしれませんが、人々が個人的な利益のために不正に操作しそうにない、合理的なプロキシを使用することで進歩できます。たとえば、チームは今四半期に提供したい機能セットを持っているかもしれません。報告された欠陥や緊急リリースを減らしたいかもしれません。または、定期的なリリースの頻度を増やしたいかもしれません。同時に、Test Certifiedのようなプログラムを導入して、テストメトリクスを収集し、ポリシーを確立し、テスト目標に向けて取り組んでください。時間の経過とともに、Test Certifiedのような目標の進捗状況は、他のチームの目標の進捗状況と相関関係があるはずです。論理的なレベルでは、相関関係は因果関係を証明しませんが、経験的なレベルでは、ユニットテストがプラスの影響を与えたという理解があるはずです。
テストによって発見されたために発生しなかったすべての問題、またはテストを選択して作成中に開発者が明らかにしたすべての問題を測定することは非常に困難です。リリースサイクル速度、ロールバック頻度、報告されたバグなどの副作用を測定できますが、テストを実施していないプロジェクトの開始時には、そのメリットの多くを信頼に基づいて受け入れることになります。一部のチームでは、数か月間ユニットテストを試し、開発者へのアンケート(「コードがより健全になっていると感じますか?変更が本番環境で何かを壊すのではないかと心配することが減りましたか?テストを作成中にバグを発見しますか?」)でフォローアップすることで、懐疑的な管理層にテストがチームを支援していることを示すことができるかもしれません。
立ち上がる
もし人々があなたの結果を却下し、ユニットテストの有効性のさらなる証拠を要求するなら、開発者の信頼、生産性、幸福度の向上、そしてそのようなデータが入手可能であればユーザーの幸福度の向上を指摘してください。懐疑論者は、他の要因に関して、これらの同じ結果と、顧客に提供される最終的な価値を測定できるでしょうか?プログラミング言語の選択?コードエディターの選択?忘年会、会議、オフサイト、ボーナス?もし私たちがその実践を正当化し、その価値を確認するための確かなデータがないことを理由にテストを批判するつもりなら、徹底的にすべてのこれらの他の技術的およびビジネス上の決定に費やされた学術研究と、それらの影響を証明する反論の余地のない経験的な証拠を生み出すべきです。
重要なのは、成功するチーム、製品、ビジネスを生み出すには多くの要因が関わってくるということです。それらのすべてが完全に測定できるわけではありませんが、それらの効果の合計が部分の合計よりも大きいという感覚があるため、それらを採用しない理由はありません。ユニットテストは、その点で他の多くのビジネス要因と変わりありません。
もし人々が、自分たちの状況が「異なる」ためにユニットテストは機能しないと主張しようとするなら、そのような根拠のない却下に立ち向かいましょう。ユニットテストの有効性を保証する多くの異なる経験を持つ多くの人々がいるという強みの立場から議論できます。それらの人々、チーム、製品、経験の間の違いは、ユニットテストの有効性の議論を弱めるのではなく、むしろ強化します。「違い」によって提示されたユニットテストへの課題を克服するために必要な唯一のことは、試す勇気です。理由を装った臆病さを暴露する機会を惜しまないでください。
冗長になり、繰り返し言う
すべての人があなたの最初の主張に反応するわけではありません。人はそれぞれ異なる説明に異なる反応を示すでしょう。さまざまな人に自分の考えを伝えるための最良の方法を見つけるために実験しましょう。新しい聴衆にアピールする方法を見つけるために、自分の考えを練り続けましょう。
月への旅
達成可能な短期目標に向けて段階的なステップから始めましょう。しかし、時が来たら、大きな目標を設定して、それに向かって飛躍することを恐れないでください。Testing Groupletは、講演会を開催し、社内研修資料を作成し、書籍を配布することから始めました。やがて、より大胆になり、Testing on the Toiletを立ち上げ、Testing Fixitsを実施しました。大規模なTest Certified戦略が定まったのは、数年にわたる実験の後であり、TAPが仕事を終えるまでにさらに数年かかりました。私たちの成功は、まず少数の熱心なボランティアを募集し、協力して強固なコミュニティとツールおよび実践の基盤を構築し、次に非常に積極的な目標を設定することによって達成されました。
あなたの努力も同様の軌跡をたどるはずです。チームメイトが十分に集まり、チームの測定基準、ポリシー、目標が定まったら、「地球上に人がいる」状態です。これで、より高い目標を設定できます。チーム、あるいはオフィスの複数のチームでFixitを組織し、目標、タスク、各タスクに割り当てられた人々をスプレッドシートで管理できます。ユニットテストに焦点を当てたイベントを成功させたことで、あなたは今「軌道上に人がいる」状態です。そのイベントからの教訓を振り返り、それによって生み出された勢いに乗って、「月に人を送り込み」、会社全体を変えるチャンスを掴む準備が整いました。
そして、あなたの会社が潮流に乗れば、もしかしたら「火星に人を送り込み」、業界全体を変えるチャンスがあるかもしれません。それは考える価値のあることです。
粘り強く
ユニットテストの価値を人々に納得させるのは必ずしも容易ではありません。あなたのサポートネットワークに頼ってください。彼らに強化と救済を頼ってください。あなただけで戦う必要はありません。委任しましょう。あなたが充電している間、他の人に主導権を握らせましょう。誰かがあなたに頼る必要があるときは、再び立ち上がる準備をしてください。
最後までやり遂げる
たとえあなたが最も野心的な文化を変える目標を達成したとしても、仕事は決して終わりません。健全な文化は注意深いメンテナンスを必要とし、ユニットテスト文化の確立を超えた次の大きなステップは、優れた自動テストの美学を教えることです。これはユニットテストレベルに限定されません。高品質なコードを保証するために、ユニットテスト、結合テスト、システムテストの全範囲を適用する必要があり、人々は各テストレベルの適切な適用とベストプラクティスについて教育される必要があります。覚えておいてください。人々は自動テストを採用することを納得するかもしれませんが、それは彼らがそれをうまく行うことを保証するものではありません。
指導やフィードバックがないと、人々は整理されていないコードや、質の悪いAPIに対して、非常に手の込んだ、複雑で、保守が難しいテストを書いてしまう可能性があります。重量級の統合テストで些細なことをチェックしてしまうかもしれません。間違ったことをチェックしてテストが不安定になったり(例えば、ページが読み込み完了したかどうかを確認するのではなく、特定のピクセルが青色かどうかをテストするなど)、不必要な冗長性があったりと、コストが高く、価値の低いテストを書いてしまう可能性もあります。良いアイデアを少しばかりやり過ぎたり、悪い方向に進めてしまう人は必ずいます。しかし、それはそのアイデアに価値がないことを意味するのではなく、どのアイデアも単独では乱用されないわけではないということです。
適切に書かれた自動テストのスイートは、コードが意図したとおりに動作すること、そして(自動テストの実施によって生じる設計上のプレッシャーのおかげで)コードがきちんと整理され、疎結合になっていることを確認するための強力なツールとなります。しかし、自動テストは他のスキルと同様に、習得には時間と努力が必要であり、常に改善の余地があります。目標は、どんな手段を使ってでも完璧なテストカバレッジを達成することではなく、開発を可能な限り効率的、効果的、そして信頼性の高いものにすることです。自動テストの価値を人々に理解させたら、テストがこの目標を達成するのに役立つように、彼らのスキルを継続的に向上させるのを助けてください。
報酬と認識
人生で最も残念なことの一つは、重要なこと、変化をもたらすことに人生を注ぎ込んだにもかかわらず、その努力がほとんど無視されること、あるいはさらに悪いことに、他の誰かがその功績を横取りすることです。真の変化を起こすために一緒に立ち上がっている人々にそんなことをさせてはいけません。大げさにする必要はありませんが、人々の努力が認められ、感謝されていることを伝えることを習慣にしましょう。
楽しもう
野心的な目標を達成する上で、楽しさの力を決して過小評価してはいけません。楽しさは負担を軽減し、人々を結びつけます。楽しさは、孫たちに、あるいは少なくとも、そのプロジェクトが終わってからずっと後にチームや会社に加わる新しいジュニア開発者たちに語りたくなるような素晴らしい物語を作ります。
最後に
もし、単体テストが特別なツールを必要とせず、新しい言語ですべてを書き直す必要もなく、他のツールやプラクティスの適用を強化し、既存のコードに段階的に適用でき、他の新しい技術や製品ドメインを学ぶ以上のコストがかからず、世界で最も複雑な開発業務の一つで当然の文化規範となり、他のあらゆる可能な安全策やテストの層をすり抜けてしまう可能性のある壊滅的なバグを検出し、または防ぐことができるとしたら、大きな疑問は
なぜ単体テストがすべての開発文化の一部ではないのか?
プログラマーやチームの中には、単体テストやそれが自分たちにもたらすことができることを知らない、経験がない、あるいは始めるための手助けを必要としている人たちがいます。この記事が、単体テストの実践を採用するよう彼らを説得する説得力のある議論になることを願っています。励みになる例として、OpenSSLプロジェクト自体が、単体テスト/自動テストのカバレッジを増やすための私の申し出に前向きに対応してくれています。そして私は、この取り組みに協力してくれる人々を積極的に募集しています。
さらに言えば、開発者が単体テストを書かないのは、彼らのチームや会社が、せいぜいテストの欠如を容認しているだけで、最悪の場合、積極的にテストを妨げているからです。そのようなチームは、しばしば「テストをする時間がない」とか、自分たちのコードは「テストするのが難しすぎる」と主張します。これは、意図的な無知、無関心、過去の悪い経験、企業のインセンティブ構造やプレッシャー、あるいはステレオタイプなカウボーイコーダーの男らしさによるのかもしれません。主な動機が何であれ、その効果は現状維持です。安易な道を選ぶことを正当化するのです。「バグは起こるものだ」と受け入れる方が、自分の習慣や周囲の文化を変えるよりもずっと楽だからです。そして、一般の人々はそれをもっともらしい言い訳だと受け入れてくれます。彼らにはそれ以上のことを知る合理的な方法がないからです。
開発者として、十分な情報を持っていない人々から(おそらく不当にも)信頼を寄せられている管理者として、私たちはこれよりも良い仕事をしなければなりません。安易で都合の良い言い訳は、私たち全員が論争の不快感をやり過ごすのに役立つかもしれませんが、それは真に生産的なことを何も達成しません。
単体テストの導入に対する文化的障害を克服するために立ち上がらなければ、私たちは、費用がかかり、恥ずかしく、潜在的に危険なソフトウェアの欠陥を防ぐという現実の課題に適用できる、最も効果的な開発ツールの一つを差し控えることになります。仲間のプログラマーへの共感(そして、私たち自身が判断されることへの密かな恐れ)によって判断を曇らせ、予防可能な欠陥を当然のこととして許容するという、手早く都合の良い結論を導き出すことで、私たちは、単体テストがない状況でそのような欠陥を生み出すプログラマー、チーム、または会社と同様に、結果として生じるあらゆる損害について同じように罪を犯すことになります。
「文化」は、技術的な失敗について私たちが提供できる最も曖昧な説明であり、特にソフトウェア開発のバックグラウンドがないユーザーにとってはそうです。そのため、マスコミは理解できる理由を喜んで受け入れ、記事を作成します。理解することで、出来事をコントロールしているという錯覚が得られるからです。また、文化は、私たち自身が与えることができる最も不快な理由でもあります。文化の失敗を認めることは、外部要因を指摘することによって大きく避けられる、アイデンティティに基づいた心理的な対立を引き起こすからです。しかし、この不快な真実を避けることは、「goto fail」やHeartbleedを引き起こした過信を容認し続けることであり、それらがビジネスを行い、効率的、安全、そして安全に生活するための手段としてますますソフトウェアに依存するようになった、ほとんど気づいていない、信頼している社会に与えた損害を容認し続けることです。
手を洗うだけでは医者にはなれないし、患者を救うこともできません。しかし、手を洗わない医者を私たちは信頼しないでしょう。ソフトウェア開発者として、私たちはユーザーのために同様の注意義務を負うべきです。単体テストなしで開発されたソフトウェアは、誰も信頼すべきではありません。
さらに読む
私のブログ、mike-bland.comには、goto failやHeartbleedバグに関する以前の多くの記事があります。この記事に最も直接的な影響を与えた以前の記事は、私のGoto Fail、Heartbleed、そして単体テスト文化ページに掲載されています。
また、Googleで単体テストがもたらした変化や、Googleの開発およびテストツールとプロセスについても、「whaling」シリーズで詳しく書いています。
私がテスティンググループレット、フィックスイットグループレット、テスト傭兵に関わっていたときに、私の考え方に最も影響を与えた本の一つは、ソール・アリンスキーのRules for Radicalsでした。特に、人々が問題を解決する力があると信じなければ、問題を解決しようと考えることすらないという彼の考えを言い換えるのが好きです。テスティンググループレットなどは、Googleの開発者にコード品質問題を解決する力を与えること、そしてその力を型破りなボトムアップ方式で提供することでした。
また、私はロバート・グリーンの大作権力に彩られた48の法則と戦争の33の戦略も好きでした(そして今も好きです)。グリーンは現代のマキャベリのような存在で、その学術的な幅広さと深さと同じくらい、その謝罪のない操作性で印象的なトーンで書いています。私は常に操作的な側面は娯楽と衝撃のために誇張されていると考えています。人間の本質に対する鋭い洞察は、特にこれらの本が、プロセスの中であなた自身の心と精神を保護するための優れたアドバイスを提供するため、人々の心と精神を変えるという任務を遂行しようとするすべての人にとって非常に価値があります。
ロバート・チャルディーニの影響力の武器は、なぜ私たちがしばしばその瞬間の熱意に駆られて説得に屈し、後になって後悔の念に駆られるのかを説明する非常に明確な本です。この本は、誰かがあなたの潜在意識レベルで積極的に影響を与えようとしているときを認識するためのツールを提供します。これは、あなた自身の環境に変化をもたらすのに役立つかもしれません。「説得のプロ」が採用している信頼メカニズムは、一般的に私たちが種として生き残り、繁栄することを可能にしてきましたが、善のためにも悪のためにも簡単に悪用される可能性があります。これを読むと、グリーンの教訓がさらに理解できるようになる幅広いフレームワークが得られます。
ジェフリー・A・ムーアのキャズムは、一見すると、技術市場の明確なセグメントを認識し、それらに直接マーケティングを行い、有望な技術製品を破滅させる「キャズム」に陥るのを避ける方法について述べています。この教訓は、ソフトウェア開発コミュニティのさまざまなセグメントが、さまざまな説得方法にどのように反応するかを理解するのにも同様に適用できます。言い換えれば、分割して征服し、「遅滞者」については忘れてください。彼らは最終的には引きずられてくるでしょう。
孫子の孫子の兵法は、自然や地形に逆らわず、それらと協力する方法を教えています。中でも最も重要なのは、「戦わずに勝つのが最善である」と強調していることです。
技術的な話に戻りますが、デビッド・A・ウィーラーの次のHeartbleedを防止する方法は、将来同様のバグを防ぐための多くの技術的なアプローチについて詳しく説明しています。彼は「ネガティブ」な単体テストの価値を指摘し、多くの開発者が実際には予想される失敗シナリオに対する「ネガティブ」なテストケースを書くことを検討していないと強く示唆しています。
ショーン・キャシディの瞑想は、ローマ皇帝マルクス・アウレリウスの瞑想に触発されたもので、読む価値があり、定期的に読み返す価値のあるソフトウェア開発原則の集まりです。単体テストや他の形式の自動テストは、それらの多くを補完します。例として、「オーバーヘッドをプラスに保つ」という原則があります。単体テストはオーバーヘッドですが、潜在的なデバッグ時間(および睡眠不足、信頼の喪失、収益の損失...)を大幅に節約します。
私がユニットテストについて学んだことのほとんどは、個人的な経験と、Testing Groupletへの参加から得たものなので、ユニットテストに関する具体的な書籍をお勧めするのは難しいです。あちこちから知識の断片を吸収したからです。とはいえ、マーティンは、彼の古典的な著書であるリファクタリング:既存のコードのデザインを改善するを含め、良い本を何冊か書いていることで知られています。この本の中のアイデアは非常に影響力が強く、他の場所でそれらについて学んだ場合でも、すべてがこの1冊の本から始まったとは信じがたいほどです。Gerard MeszarosのxUnitテストパターンについては、マーティンの代表的なシリーズの一部として出版されたもので、多くの良い評判を聞いています。私のAutomated Testing Boston Meetupの同僚であるStephen Vanceは、この記事を書く数ヶ月前に、高品質なコード:ソフトウェアテストの原則、実践、およびパターンを出版しました。Testing on the Toiletの公開されているエピソードも、自動テストの知恵を得るための非常に役立つ情報源です。
結局のところ、テストは良いコードを書くのに役立つことを目的としているため、ユニットテストに焦点を当てていなくても、良いコーディングと設計の実践に関する本を読むことには多くの価値があります。実際、1つか2つの本ですべてを教えてもらうことを期待するよりも、多くの情報源から得たアイデアを自分のコードに適用し、それらをすべてうまく融合させることには意味があります。そのために、駆け出しのC++開発者として、私はHerb SutterのExceptional C++シリーズ、Scott MeyersのEffective C++シリーズ、Bjarne StroustrupのThe C++ Programming Language、Brian KernighanとDennis RitchieのThe C Programming Language、Cormen、Leiserson、RivestのIntroduction to Algorithms(Steinが共著者になる前)、そして前述のGang Of Fourの本、別名Design Patterns: Elements of Reusable Object-Oriented Softwareを読みふけりました。私のNorthrop Grummanのチームメイトは、私が毎日ジムバッグに入れて職場に持ち歩く「図書館」をからかいました。これらの本に没頭する中で、私はそれらのアイデアをプロダクションコードに適用し始めただけでなく、このエキサイティングな新しいコードを徹底的にユニットテストするようになりました。その経験は、私の初期のユニットテストによって得られた迅速なフィードバックのおかげで、情報をより深く吸収するのに役立ち、ユニットテストの価値を永遠に確信させました。
謝辞
この記事は、私の名前が著者名に書かれているにもかかわらず、何十人もの人々の寛大で注意深い努力の結果であり、その中には共同著者のクレジットを共有すべき人もいるでしょう。
ただ、彼らの多くがこの記事の長さに責任があると言わざるを得ません。私が書きすぎたこと、そしてレビューアがそれを短くするのに役立つだろうという予想にもかかわらず(そして、公平に言えば、彼らが言葉遣いを大幅に改善するのに役立った場所はたくさんありました)、彼らの熱心で洞察に満ちた提案の多くを私はそのまま取り入れました。
Charles Balloweは、ユニットテストケースの設計はインターフェース契約によって推進されるべきだが、コーナーケースやエラー処理の弱点を調査するために実装の知識を使用する必要があるという私の主張を明確にするように私に求めました。彼は「バグのあるテスト」の例を提案し、イントロダクションで私の「事後分析/回顧」の意図を明示するように私に思い出させました。Tony Aiutoは、私が最終的に「goto fail」ユニットテストで使用した「ポインタエンコーディング」のトリックを私に見せてくれました。John Penixは、Googleの「ベテラン」たちが、テスト文化導入前のすべてを正しく行っていたと考えていたことを私に思い出させ、TAPに関する私の主張を再確認しました。Christian Kemperは、Googleに関する私のコメントが、私が退社してからの進展に照らして混乱を避けるために、私がそこにいた時代に関連していることを確認することに注意を払いました。John Turekは、テストが実装を「ミラーリング」する必要がある場合(最も低いレベルではまれに)と、そうでない場合(ほとんどの場合)の境界線をどこに引くかを明確にするように、そしてユニットテストはコードを書いている間に行われるべきで、後から行うべきではないことを明確にするように私に求めました。Stephen Ngは、私がイントロダクション、「goto fail」、「Google Retrofitted」のセクションで、どのような態度を示し、どのような議論をしたいのかを正確に明確にするように私に求めました。Sverre Sundsdalは、「ユニットテストがどのように役立ったか」セクションで具体的な原則を詳しく説明し、TAPの力を強調することを提案しました。Adam Sawyerは、読者が「goto fail」セクションから引き出す可能性のある意図しない政治的な結論を避けるのに役立ちました。Alex Buccinoは、過去、現在、未来のすべてのRelEngに対する自動テストに関するRelEngの見解を明確にしました。Rich Martinは、「ツール」セクションの「職人技」の段落をほぼ逐語的に提供し、「パートナー・イン・クライム」と「可視性の向上」の2番目の段落も提供しました。Alex Martelliは、「ツール」セクションのイントロダクションで示されている、並行性の問題に対するユニットテストの難しさを指摘しました。Sean Cassidyは、ドキュメントが「ツール」に含まれる価値があることを思い出させてくれました。Lisa Careyは、システムをドキュメント化するのが難しい場合は、その設計に問題があることが多いと指摘しました。Jessica Tomechakは、コードレビューがドキュメントに与える利点を指摘しました。Ana UlinのGoogleを離れて新しい会社での経験に対する視点は、「文化を変える方法」セクションを作成する動機となり、私は彼女の元のアイデアの書き換えとして「焦点を維持する」を追加しました。Patrick Doyleは、「文化を変える方法」セクションに多くの素晴らしいアイデアを提供し、「徹底的なフォローアップ」のサブセクションにも影響を与えました。Adam Wildavskyは、記事全体を通して彼のコメントで特に徹底的で、文法の改善を提案し、私の議論の一部に異議を唱え、含めるべき追加の資料を私に与えてくれました。
上記の多くの方々は、記事の他の側面を改善するのに役立つ、他の多くの非常に役立つコメントもしました。私は彼らの最も顕著な貢献の一部に対して公正な評価を与えたいだけです。
特定のセクションまたはドキュメント全体について広範なコメントを提供したその他の人には、Kendra Curtis、Aran Donohue、Alan Donovan、Chris George、Larry Hosken、Rob Konigsberg、Chris Rohrs、Gregor Rothfuss、Matt Simmons、Andrew Trenk、Glenn Trewitt、Gene Volovich、Zhanyong Wan、Col Willisが含まれます。
私はまた、見てフィードバックや承認を提供してくれた他の人々にも感謝しています。Alex Aizikovsky、Jason Arbon、Dave Astels、Andrew Boyer、RT Carpenter、Mathieu Gagné、Chris George、Joseph Graves、Paul Hammant、Mark Ivey、Bryan Kinney、Tayeb Karim、Camilo Arango Moreno、Brian Okken、David Plass、C. Keith Ray、Steve Schirripa、Isaac Truett、Stephen Vanceです。
この記事に直接協力してくれた人々に加えて、Googleの開発文化を変えるのに貢献してくれた人々が遥かに多くおり、それがこの記事や私がこのテーマについて書いた他の記事の基礎となりました。私のブログの投稿全体を通して、彼らに公正な評価を与えるためにできる限りのことをしてきました。彼らには、Testing Groupletのメンバー、Test Mercenaries、Test Certifiedのメンターとチーム、「Testing on the Toilet」のメンテナーと貢献者、Testing Techチーム、Build Toolsチーム、および以前のエンジニアリング生産性部門のメンバーが含まれます。これらのグループを超えて、何らかの形でこの原因に貢献した多くの同類もいました。特に以下の人々を呼び出したいと思います(このリストに名前が載っているべき、または載っているべきではないと思う場合はお知らせください。それに応じて更新します)。
Adam Abrons、Ulf Adams、David Agraz、Mohsin Ahmed、Tony Aiuto、Alex Aizikovsky、Vishal Arora、Dave Astels、Venuprakash Barathan、Milos Besta、Jennifer Bevan、Tracy Bialik、Carla Bromberg、Dennis Byrne、Michael Chastain、Araceli Checa、Deanna Chen、Dianna Chou、Alex Chu、Kevin Cooney、Patrick Copeland、Jay Corbett、Bradford Cross、Kendra Curtis、Pavithra Dankanikote、Kelechi Dike、Alan Donovan、Patrick Doyle、Peter Epstein、Ambrose Feinstein、Simon Quellen Field、Daniel Fireman、Ariel Garza、Nachum Goldstein、Nikhil Gore、Brad Green、Misha Gridnev、Christian Gruber、Paul Hammant、Matt Hargett、Johannes Henkel、Johannes Henkel、Miško Hevery、Gregor Hohpe、Jason Huggins、Susan Hunter、Mark Ivey、Ralph Jocham、Emily Johnston、Michał Kaczmarek、Tayeb Karim、Nitin Kaushik、Christian Kemper、Maria Khomenko、Wolfgang Klier、Erik Kline、Damon Kohler、Rob Konigsberg、Nicolai Krakowiak、David Kramer、Archana Krishna、Deepa Kurian、Jonny LeRoy、Mike Lee、Flavio Lerda、Nick Lesiecki、Michelle Levesque、Kimmy Lin、Mindy Liu、Chris Lopez、David Mankin、Alex Martelli、Rich Martin、Thomas McGhan、Jim McMaster、Bharat Mediratta、Boyd Montgomery、David Morganthaler、Sam Newman、Steve Ng、Eric Nickell、Robert Nilsson、Neal Norwitz、Andy Watson Orion、Rong Ou、John Penix、Rob Peterson、Antoine Picard、James Pine、David Plass、Rachel Potvin、Simon Pyle、Kevin Rabsatt、C. Keith Ray、Tim Reaves、Thirumala Reddy、Mamie Rheingold、Phil Rollet、Gregor Rothfuss、Russ Rufer、Thomas Rybka、Nick Sakharov、Diego Salas、Thiago Robert Santos、John Sarapata、Steve Schirripa、Eric Schrock、Roshan Sembacuttiaratchy、Meghan Shakar、Craig Silverstein、Matt Simmons、Dave Smith、Matthew Springer、Kurt Steinkraus、Bill Strathearn、Mark Striebeck、Cristina Tcheyan、Jean Tessier、John Thomas、Jessica Tomechak、Andrew Trenk、Glenn Trewitt、John Turek、Scott Turnquest、Ana Ulin、Matt Vail、Gene Volovich、Zhanyong Wan、Lindsay Webster、Chris Van Der Westhuizen、Nicolas Wettstein、Adam Wildavsky、Collin Winter、Jonathan Wolter、Julie Wu、Kai Xu、Runhua Yang、Noel Yap、Jeffrey Yasskin、Catherine Ye、Nathan York、Paul Zabelin、Henner Zeller。
私がよく言うように、魔法の杖を一振りして物事を実現するスーパーマンなど存在しません。変化を起こすには、私たち全員が長年にわたって協力する必要がありました。しかし、それは実現しました。この世にチームワークほど強力な魔法はありません。
そしてもちろん、私に断れない申し出をしてくれたMartin Fowlerにも感謝します。私はもともと、私のACM Queueの記事となった「リンゴの中に1つ以上の虫を見つける」をレビューしてもらうために彼の助けを求めました。彼は最終的に、この記事を投稿するように私に提案し、彼がもともと要求していると私が想定していたよりもはるかに広い範囲と構造を定義しました。最終的な製品は、彼のビジョンと指導の直接的な結果であり、私が一人で作成することは決してなかったものです。
大幅な改訂
2014年6月3日:「文化を変える方法」と「最終的な考え」のセクションと付録を公開
2014年5月29日:Googleセクションを公開
2014年5月27日:その他の便利なツールと実践を公開
2014年5月20日:コストと利点のセクションを公開
2014年5月14日:Heartbleedセクションを公開
2014年5月12日:イントロダクションとgoto failセクションを公開