test
document
テスト
研究
ドキュメント

忙しい研究者のためのテストコードとドキュメントの書き方

はじめに

「研究用のプログラムにもテストコードやドキュメント書いたほうがいいよね。」
「分かっちゃいるけど、そんな面倒なことやってられないよ。」
という研究者あるあるを解決すべく、僕が普段実践している開発スタイルを紹介します。
この開発スタイルのすごいところは:

  • テストやドキュメントを一切書かない場合と比べて追加の工数がほぼゼロ
  • 普通にコーディングしているだけで、いつのまにかテストコードとドキュメントまでできあがっている。
  • 実装、コメント、テスト、ドキュメントが自然に同期するので、保守しやすい。

Pythonを例に紹介しますが、コメント内でテストできるツールと、コメントからドキュメントを生成できるツールをもつ言語ならばどれにでも応用できるはずです。

この開発スタイルに至った背景

ソフトウェア開発において、テストコードやドキュメントを整備することでプログラムの品質が向上することは広く知られています。その有効性は、研究用のプログラムであっても変わりません。それにもかかわらず、テストコードやドキュメントのない研究用プログラムをよく見かけます。

試しに Twitter でアンケートとってみたら、


  • テストを書く人は約4割
  • ドキュメントを書く人は約6割

という結果になりました。非常に荒っぽく言い換えれば、研究で使うプログラムの

  • 約6割にはテストがなく、
  • 約4割にはドキュメントがない

ということになります。1

研究者は多忙だからしょうがない? いや、本当に忙しいならなおさら、品質の高いプログラムを書いて、バグによる手戻りを避けたほうがいいはずです。なぜそうしないかといえば、普通のテストやドキュメントの方法論は、研究者にとってメリットが少ない(むしろ生産性が下がる)からではないでしょうか?

基本的に、世間に流通するテストやドキュメントの方法論は、チーム開発向けに作られています。チーム開発の方法論には「自分の生産性を下げて他人の生産性を上げる」利他的なプラクティスが多く含まれています。

alt

個人開発が主となる研究者にとっては、そのような利他的なプラクティスは報われにくいです。割いた工数のぶんだけ、生産性をむしろ下げる要因になることが多いです。

alt

そもそも研究で作るソフトウェアには仕様変更がつきもの。保守のコストも通常のソフトウェアよりも高くつきます。最初のうちはやる気でなんとかなっても長続きしません。なので、チーム開発向けに培われた方法論の中から「自分自身の生産性が上がる」ものだけを採用し、「最小限の手間で継続できる」ようにアレンジして実践するのが有効ではないかと思います。いわば、自分で自分の生産性を引き上げるブートストラップ効果を狙うのです。

alt

この開発スタイルを実践することで得られる効果

  • バグが減り、開発が早くなる。
  • 他人にコードを引き渡す際に、改めて説明資料を用意する手間が省ける。
  • 他人にコードを引き渡した後、問合せが減り、より自分の仕事に集中できるようになる。

ではいってみましょう!

Step 0: 環境構築

まずはテストとドキュメントの環境を構築します。

テスト環境:doctest

Pythonの標準ライブラリなのでインストール作業不要です。

ドキュメント生成:Sphinx

pip でインストールします。

$ pip install Sphinx

まずはドキュメントの雛型を生成します。

$ sphinx-quickstart

生成された docs/conf.pyextensions に下記の拡張を追加します:

  • sphinx.ext.napoleon
    • docstring が numpydoc 形式や googledoc 形式で書けるようになります。
    • これから紹介する開発スタイルでは doctest を沢山書くので、docstring のインデントが浅い numpydoc 形式と相性が良いです。

追加した結果はこうなります:

docs/conf.py
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.todo',
    'sphinx.ext.viewcode',
    'sphinx.ext.napoleon',
]

んでは実装スタート!

Step 1: 関数スタブを書く

何も考えずに関数を書きます。この時点では、関数名は適当、引数もなしでOKです。そんなものは後から考えます。

examples.py
def f():
    pass

Step 2: docstring を書く

いきなり docstring を追加し、さっき決めた関数名も引数も無視して「この関数はこんな風に使いたいな~」という願望を書きます。そうです、docstring を API 設計のメモ帳として使うのです。

examples.py
def f():
    """
    >>> print_sum(1, 2)
    3
    >>> print_sum(-1, 2)
    1
    """
    pass

Step 3: doctest する

API の仕様が決まったら、とりあえずテスト。
えいや!

$ python -m doctest examples.py
**********************************************************************
File "examples.py", line 3, in examples.f
Failed example:
    print_sum(1, 2)
Exception raised:
    Traceback (most recent call last):
      File "/usr/lib/python2.7/doctest.py", line 1315, in __run
        compileflags, 1) in test.globs
      File "<doctest examples.f[0]>", line 1, in <module>
        print_sum(1, 2)
    NameError: name 'print_sum' is not defined
**********************************************************************
File "examples.py", line 5, in examples.f
Failed example:
    print_sum(-1, 2)
Exception raised:
    Traceback (most recent call last):
      File "/usr/lib/python2.7/doctest.py", line 1315, in __run
        compileflags, 1) in test.globs
      File "<doctest examples.f[1]>", line 1, in <module>
        print_sum(-1, 2)
    NameError: name 'print_sum' is not defined
**********************************************************************
1 items had failures:
   2 of   2 in examples.f
***Test Failed*** 2 failures.

当然テスト失敗。それでいいんです。

Step 4: 実装する

テストが通るように実装を直しましょう。

examples.py
def print_sum(x, y):
    """
    >>> print_sum(1, 2)
    3
    >>> print_sum(-1, 2)
    1
    """
    print(x + y)

実際の開発ではステップ3と4を行き来します。テストケースと実装を徐々に追加して、関数を完成させます。具体的にどうやってステップ3と4を回すかについては、テスト駆動開発を参考にしてください。

Step 5: API コメントを書く

完成したら、API のコメントを付け、体裁を整えましょう。

  • 1行概要
  • 詳細説明
  • パラメータ
  • 戻り値(あれば)

すると、さっきまで書いていた doctest は…あら不思議、そのまま用例の説明に変身してしまいました。

examples.py
def print_sum(x, y):
    """Print x + y.

    This function sums up two arguments and prints the result.
    The addition is done by ``+`` binary operator.

    Parameters
    ----------
    x : object
        A value to be added and printed. Must support addition with ``y``.
    y : object
        A value to be added and printed. Must support addition with ``x``.

    Examples
    --------
    >>> print_sum(1, 2)
    3
    >>> print_sum(-1, 2)
    1
    """
    print(x + y)

Step 6: ドキュメントを生成する

さて、コメントも書きあがったので、ドキュメントファイルを生成しましょう。

$ cd docs
$ make html

これで完了。
ドキュメントのためにわざわざ追加作業をしなくても、きれいなドキュメントが出来上がりました。ドキュメントには API の説明から使用例まで揃っていて、一級品のパッケージと見比べても遜色ないですね。

Step 7: (オプション)もっとしっかりテストする

みんなで使うライブラリ等を作っているなら、もう少しテストを書いたほうがいいかも。そのときは、pytest などを使ってカバレッジを意識したテストケースを書くといいです。正常系は doctest でテスト済みなので、異常系と境界値のテストをするとよいと思います。これらのテストは API の本質ではないので、docstring には含めず、別ファイルに分けて書きましょう。

ただし、頑張りすぎは禁物。異常系のテストはとっても難しいです。テストケース(どんな例外があるか)を洗い出すには言語やライブラリの仕様に精通していないといけないし、網羅的なテストは一研究者の工数では手に負えません。無理せず、その道のプロにお願いしましょう。

これでなぜ生産性が上がるの?

理由1: テスト駆動開発

以上の開発スタイルは、テスト駆動開発になっています。

  • 先にテストを書いて、そのテストをパスするように開発を進めることで、早期にバグが見つかり、作業の手戻りが少なくなります。
  • 先に使用例を書くことになるので、自然に使いやすいインターフェースになり、将来このコードを利用するコードも書きやすくなります。
  • テストによって何を実装すべきかが明確になり、実装で悩む時間が短縮されます。

理由2: コメントの徹底的な再利用

コメントを書いても機能は増えません。なので、コメントに費やす工数をできるだけ小さくすることで生産性は向上します。

この開発スタイルでは、一度書いたコメントを様々な意味に再解釈していくことで、コメント作業に費やした工数を仮想的に減らしています。

  • はじめ、コメントはAPI設計のメモ書きとして生み出されます。
  • そこで書いたコメントは、そのままテストケースとして再利用されます。
  • そのテストケースは、そのままドキュメントにおけるAPI使用例として再利用されます。

さらに、こうして出来上がったドキュメントは、自分がさらに開発を進めるためのリファレンスとして利用できます。

理由3: 実装、コメント、テスト、ドキュメントが自然に同期する

実装、コメント、テスト、ドキュメント…すべての内容が食い違わないように維持することは、手作業ではとても手間がかかります。どれか1つを書き換えたら、他がシステマチックに同期してくれるような仕組みが必要です。

この開発スタイルでは、コメント⇒テスト⇒実装⇒ドキュメントという連鎖で、すべての要素が同期することを保証します。

  • いずれかの要素に対して何か変更するときは、必ず最初にコメントを書き換えます。コメントだけは、他の要素から生成できないからです。
  • コメント(の中の API 使用例)を変更すると、それに合わせてテストが自動的に変更されます。
  • テストが変更されると、実装がテストにパスしなくなるので、実装を書き換える必要が生じます。
  • 実装変更によって API が変わった場合には、API コメントを書き換えるので、それに合わせてドキュメントも変更されます。

以上の作業が1ファイル内で完結することもポイントです。変更箇所が1か所にまとまっているため、あちこちのファイルを開いて回る手間がなく、変更漏れによるバグも生じにくいです。


  1. 一人ひとりが書くプログラムの量が同じだと仮定して、Twitter アンケートのバイアスも無視した荒い近似です。