たった1人から始める社内テストコード文化


テストコード文化がない会社に勤めるサラリーマンエンジニアが、マネージャーの顔色を伺いつつテスト文化を広めるために頑張る話。記事最後にテストをコードレビューする際の観点を書いています。

新規開発でテストコードを書く提案は否決された

『今回はリリース優先のプロジェクトである。』マネージャーの一言でチームでテストコードを書く案は却下されてしまった。社内にテストコードを書くノウハウがないため冒険すべきじゃないって理由はよく理解できた。ただ個人で空いた時間にテストを書くのは問題ないはずなので出来る限り頑張ろうと思いました。

たった1人から始めるテストコード文化

知ってるエンジニアに聞いて周ったらテスト書いたことある人が2人くらいいた。本人には言わないけどメンター認定してPythonとDjangoで効率のよいやり方とノウハウを教えてもらう。振り返ると環境に恵まれていたんだと思います。Pythonのテストフレームは複数あるけど公式ドキュメントがしっかりしているPytestを採用。Pytestは高機能でドキュメントが複雑なので、使い方だけExampleで覚えて、辞書的に利用していく上手く学べると思う。

pytestのインストール
pip install pytest
pytestのテストサンプル
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals


# テストする関数
def add(a, b):
    return a + b


# テストコード 関数名はtest_ から始めるのがpytestでのお作法
def test_add():
    assert add(1, 1) == 2
    assert add(1, 2) != 2
pytestの実行結果
>>> $ py.test ../tests/test_add.py 
=============================================================================== test session starts ===============================================================================
platform darwin -- Python 2.7.5 -- py-1.4.31 -- pytest-2.7.0
rootdir: /Users/***********, inifile: pytest.ini
plugins: cache, django, pep8, pythonpath
collected 2 items 

../tests/test_add.py ..

============================================================================ 2 passed in 0.06 seconds =============================================================================

テストカバレッジが上がらない

最初は単体テストを頑張って書いていたんだけど、どうも開発者の負担が大きい。頑張り過ぎ感がある。相談してみたらソシャゲのサーバなら関数単位のテストではなく、Web API単位でブラックボックステストするといいぞとアドバイスを貰った。やってみたら開発工数が少なく、シンプルで強力だった。いまでも使っています。コードカバレッジは上がりませんでしたが、WebAPIのテストは少ない工数でAPIを網羅することが出来ます。

このテスト方法はスクリプト言語で特に有効で、コンパイル言語だとコンパイル時に検出される類いのバグがよく見つかります。

JSON方式のWebAPIをテストするpytestコード
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import ujson
import requests


def test_api():
    # GitHubAPIを試験する
    url = "https://api.github.com/repos/vmg/redcarpet/issues?state=closed"
    headers = {'Accept-Encoding': 'identity, deflate, compress, gzip',
               'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0',
               'Content-type': 'application/json; charset=utf-8',
               }
    response = requests.get(url, headers=headers)
    # HTTP Statusコードが200であること
    assert response.status_code == 200

    # BODYをjsonでパースできること
    data = ujson.loads(response.text)

    # 配列内にurl, state, created_atの要素が存在すること
    for param in data:
        assert 'url' in param
        assert 'state' in param
        assert 'created_at' in param

pytest実行結果
>>> $ py.test ../tests/test_webapi.py 
=============================================================================== test session starts ===============================================================================
platform darwin -- Python 2.7.5 -- py-1.4.31 -- pytest-2.7.0
rootdir: /Users/*****, inifile: pytest.ini
plugins: cache, django, pep8, pythonpath
collected 2 items 

../tests/test_webapi.py ..
============================================================================ 2 passed in 0.87 seconds =============================================================================

開発チームをテスト文化に染めていく

サーバの全APIテストが完成した段階でチーム内で相談してWebAPIテストを書く事をルール化しました。pytestは仕組みが単純なのでサンプルコードがあれば誰でも簡単に習得できました。ただ定着まではレビューで指摘したり、定期的にテストを実行してエラーが出たら直したり、新規参画者に伝えたりと泥臭い作業が長期間必要です。(詳しい人なら定期的に実行?commit hookで自動テストだろと感じたと思います。当時は導入しようと思いつつ重い腰が上がらないまま導入を見送りました。結果起きた問題は後述しています。)

・ コマンド1つで全テストが実行できるようにした(shell書いた)
・ 気持ちよく開発するために全テストの実行は60秒以内で完了するようとにかく配慮した
・ ローカル環境構築手順で作業完了条件をテストが通ることとした
・ コードレビュー時にテストが通ることを確認した
・ 新機能開発時はコードレビュー時にテストが添付されていることを必須条件とした
・ 定期的にテストを実行して、せっせと直したり直してもらった

運用段階で祈りdeployとバグが減った!

アプリは無事リリースされ、チーム内にテスト文化が定着しつつありました。開発はpull request単位で行われ、指摘しなくてテストコードが添付されています。テスト失敗によって新規開発モジュールと影響範囲外と思われていたモジュールとの依存関係が見つかり、本番バグを未然に防ぐことができました。

運用ルールとして、deploy時のチェックリストに必ずテスト実行と明文化していたため開発段階からテストを実行する癖がメンバー間に根付いていきました。

今度は社内にテスト文化を広める

社内勉強会を開いてテスト書くといいぞと布教していきました。うちのプロジェクトが成功した理由(の1つは)テストコードを書いたこと、といった具合です。とくに社内だと実際のテストコードが共有できたので効果的でした。

終わらないテスト、実行されないテスト

時の試練といいますか、長期間運用を続けていくとテストコードは技術的負債を抱えていきます。毎回毎回変更を加えるたびに修正が必要となる問題児テスト、1度実行すると300秒以上掛かるテスト、急ぎdeploy時にテストコード自体の問題でエラーが出たためコメントアウトされたまま放置されるテストコード。修正しようにも複雑過ぎて何をテストしているか自体が判らないテストコード。

チーム内で衰退したテスト文化

運用を開始して1年。当初は60秒以内で実行が完了していたテストは、終了まで12分掛かるようになりました。deploy時にテストを実行するルールも待ち時間のため省略されています。エンジニアに古参兵が多く、デバッグ部隊も優秀だったため、それでもバグは増えませんでした。新規モジュールのプルリクにはテストを書く人が多かったのですが、プロジェクト内のテスト文化は衰退していきました。

テスト文化の種は育ちつつある

社内勉強会の影響や、一人で始めたつもりでしたが実は一部のエンジニアは既に個人でテストコードを書いていた下地もあり、徐々にテストを書くことをルールとして採用する新規プロジェクトが増えていきました。うちテストやってるよ!と声を出すことって重要だなと思います。

おわり




振り返り:どこで間違ったのか、チームが疲弊したか

ビジネスロジックの実装を怠ると仕様通り動作しないため実装必須でありますが、テストコードは実装しなくても、実行しなくてもなんとかなります。テスト文化が衰退した理由は開発チームがテストのメンテに疲弊したことに原因があるかなと思うので、どこで疲弊したか書いてみます。

1-1. 手でテスト実行したり、ローカル環境でのテスト実行はイケてない

commit hookとjenkinsを連携したり、TravisCIやCircleCIを採用してcommitしたタイミングで自動テストするようにすべきであったと思います。DjangoでのCircleCI利用方法はこちらの記事に書きました

1-2.質が悪いテストは捨て去るべきだった

複雑にマスターデータ依存しており2回に1度はテスト自体の問題で失敗するテストのメンテでチームが疲弊しました。新陳代謝のためにも質が悪いテストはコードを捨て去って書き直すべきであったと思います。

1-3. 実行時間が長いテストは省略される

60秒以上掛かるテストは待てません。開発のテンポが悪くなるのでテストの並列実行を検討したり、quick testとfull testのように2階層に分けるといった工夫で、気持ちよく開発できるようもっと配慮すべきであったと思います。

1-4. 何の観点で試験をしているかコメントを残すべきだった

長期間運用するとエラーは出るものの、変数の数字が並び何のテストか判らない書いた本人はプロジェクトから離れてしまっているといった事態が発生するので、何の観点で試験したのか試験項目を残すべきであったと思います。

2. チームでテストコードを書くためのノウハウ

2-1. 環境構築手順の完了条件にテストが正常終了することと記載する

新規参画者に有効な施策です。構築後もpull request毎にテストが通ったか確認すると、定着が早いです。

2-2. pull request開発はチームでテストコードを書く作業に効果的

pull requestベースで開発を進めると統制が効くため、テストがないコードが増える事態を未然に防げ、教育効果が大きいです。

2-3. 壊れたテストを放置しない

テスト失敗が状態化すると、だれもテスト結果を見なくなるので優先的に直します。専任で誰かが直すと、いつまで経っても改善しないため当番制で持ち回りで行うと教育にもなりよい。

2-4. テストの実行時間に気を配る。遅いテストは悪

テストの実行時間が遅いと結果を確認しなくなるため、テストの並列実行や高速化する手を打つべきです。

3. テストをコードレビューする際の観点

pull requestを受けたときにコードレビューでチェックする観点です。

3-1. テストは通るか

自動化したり、実際にcheckoutしてテストが通ることを確認します。

3-2. テスト実行時間は適切か

特殊な理由が無い限り1つのテストは5秒以内に終わるべきです。実行時間の観点で時間が掛かるテストが存在しないか確認します。

3-3. マスターデータに依存していないか、固定値はないか

card_id = 1 といった固定値は、将来IDが変更されたときエラーとなる技術的負債です。固定値ではなくfixturesでテスト用の値をロードするか、他のデータ取得APIからIDを引くよう指摘します。

3-4. 適切なテスト項目に関するコメントが残されているか

何の観点でのテストであるか記述されているかの観点で確認します。長期間運用するとメンバーが入れ替わるので、テストでエラーが出ても理由が判らなくなります。

3-5. 実行順序による依存が存在しないか

たとえば登録したあと、削除するといったテストを別個に書くと依存関係が発生します。テストの実行順序に依存があると高速化のためにテストを並列化する時にエラーとなるため、依存がある場合は1つにまとめるよう指摘します。