前回は3日目に3つ学んだことの中の2つめ「テストを整理する方法」についてまとめました。今回は3日目に学んだことの最後、3つめである「何をテストするか」についてまとめます。
このまとめはインターンシップ時に書いたメモを読み返しながら書いています。数日前にこのURLのパスを変えました。当初は「/2013/...
」と開催年を使っていたのですが、「/2/...
」と通算何回目のインターンシップかを使うことにしました。理由は2013年に2回インターンシップを実施することになったため、開催年がインターンシップを識別するユニークな情報ではなくなったからです。
テストを書いた方がよいことはわかった、テストを整理する方法もわかった、どんどんテストを書いていける。そんな状態でテストを書き始めると、たくさんテストを書いてしまいます。テストがたくさんあることはよいことのように聞こえますが、必ずしもよいこととは限りません。テストがたくさんあると以下のようなことが起きます。
どちらも「テストはいらない」と感じる原因になります。
テストが遅くなるとテストを実行することが面倒になります。面倒になるとテストを実行しなくなります。テストを実行しないと「テストがあっても意味ないね」と思うようになり、「テストはいらない」と感じるようになります。
APIの変更などでテストを変更する必要があったとき、テストが多いとテストの修正が大変になります。テストのメンテナンスが大変になるということです。テストのメンテナンスが大変になるとテストの作成・変更が開発の足をひっぱるようになり、機能の追加や修正作業に影響がでます。そうすると、自分達は「機能の追加や修正をしたいはずなのにテストばかりに時間を使っている、これでは本末転倒じゃないか」と思うようになり、「テストはいらない」と感じるようになります。
補足しておくと、どちらもテストがたくさんあることだけが原因ではありません。原因の1つというだけです。例えば、データベースに接続しているために遅くなる場合もありますし、テストが整理されていないためにメンテナンスが大変になる場合もあります。データベースに接続して遅くなっているなら、スタブを使って速くすることができますし、テストを整理することでメンテナンスを簡単にすることもできます。ここでは、テストの多さに注目するというだけです。
せっかくテストの恩恵を得るためにたくさんテストを書いたのに、「テストいらないかも…」と感じるようになってはもったいありません。そうならないために、適切な量のテストだけ書きましょう。
適切な量のテストとは「実質的に同じこと」を含まないテストです。「同じこと」ではなく「実質的に同じこと」です。
例えば、以下の2つのアサーションは「同じこと」を確認しています。
1 2 |
assert_equal(11, 2 + 9) assert_equal(11, 2 + 9) |
以下の2つのアサーションは「実質的に同じこと」を確認しています。
1 2 |
assert_equal(11, 2 + 9) assert_equal(12, 3 + 9) |
以下ようにすると「実質的に違うこと」を確認しています。
1 2 |
assert_equal(11, 2 + 9) assert_equal(-7, 2 + -9) |
実質的に同じかどうかを判断するポイントは、入力値がどの分類に属しているかです。入力値が同じ分類なら「実質的に同じ」です。
「2 + 9
」を「正の整数 + 正の整数
」と考えると、「2
」も「3
」もどちらも同じ「正の整数
」という分類に入るので、「2 + 9
」も「3 + 9
」も実質的に同じです。
一方、「-9
」は「負の整数
」という分類になるので、「2 + 9
」と「2 + -9
」は実質的に違います。
分類をどう考えるかは「何を基準にするか」で変わってきます。たとえば、偶数か奇数かという基準にすれば「2
」と「3
」は実質的に違います。
どうやって適切な基準を見つければよいか、そのやり方はまだうまくまとめられていません。テストを書いているときは、基準を考えて、分類し、実質的に違うことだけ確認しようとしているので、何かしらやり方をもっているような気がしますが、それを他の人に説明するところまではいっていません。「境界値を見つける」など説明できることはありますが、それだけではない気がしています。もっと何か別のやり方を持っている気がします。それらについてもうまく説明できるようになることは今後の課題です。
ただ、具体的にこの場合はどうするか?ということには答えることができます。1つ紹介します。
以下のようなテストがありました。
1 2 3 4 5 6 7 8 9 10 |
def test_title epub_book_doc_all = EPUB::Parser.parse(fixture_path('empty_contributors_single_spine.epub')) @document_all = EPUBSearcher::EPUBDocument.new(epub_book_doc_all) epub_book_doc_11_12 = EPUB::Parser.parse(fixture_path('single_contributors_multi_spine.epub')) @document_11_12 = EPUBSearcher::EPUBDocument.new(epub_book_doc_11_12) assert_equal("groongaについて", @document_all.title) assert_equal("groongaについて", @document_11_12.title) end |
@document_all
と@document_11_12
の違いはコントリビューターの数とspineの数です。タイトルは同じです。
このときは、テスト対象の「タイトル」を基準に分類します。タイトルに違いがないなら、コントリビューターの数が違ってもspineの数が違っても同じ分類と考えます。同じ分類なら実質的に同じです。
書いてみて気づきましたが、テストで何に注目しているかを考えることが基準を見つけるやり方のひとつのような気がしますね。まとめてよかったです。
インターンシップで説明した何をテストするかについてまとめました。インターンシップのときはうまく説明できなかったのですが、こうしてまとめてみたら整理された気がします。よかったです。
クリアコードが関わるプロジェクトの多くでは、ビルドシステムとしてAutotoolsを使用しています。そのため、新しくプロジェクトに参加した開発者にもAutotoolsに関わる修正を担当してもらうことがあります。しかし、個々の開発者のバックグラウンドは様々であり、必ずしもすべての開発者がAutotoolsに関する知識を持っているわけではありません。その上、プログラミング言語などの基礎的な知識とは異なり、学校の授業や企業の研修などでAutotoolsについて学ぶことができる機会は稀であり、まとまった解説書も少ないなどといった事情があるため、その使い方を伝授するのには毎度手間を要しているというのが実状です。
そこで、これから数回に分けてAutotoolsの使い方を解説していくことを予定しています。
Autotoolsとは、autoconf、automake、libtoolといったツールの総称です。
トップディレクトリに「configure」というスクリプトがあるソフトウェアをビルドしたことがある方はよくご存知でしょうが、このようなソフトウェアは一般的に
./configure make make install
という手順でビルドします。configureスクリプトは環境にあったMakefileファイルを自動的に生成するため、ユーザーは環境の違いを意識することなく、一定の手順でソフトウェアをビルドすることができます。ソフトウェアをソースコードで配布する場合、多種多様な環境でビルドできることが求められるため、このような仕組みが必要になります。
このconfigureスクリプトや、Makefileの雛形を生成するためのツールがAutotoolsです*1。AutotoolsはGNUプロジェクトによって開発されていますが、GNU以外のプロジェクトにも広く浸透しており、非常に多くのソフトウェアで利用されています。
Autotoolsを使うべき理由として、以下のようなことが挙げられます。
Autotoolsを使用すると、シンプルな記述で高機能なMakefileを生成することができます。
Autotools対応ソフトウェアのソースパッケージの中には、ソースコード以外のファイルが大量に格納されていて、一見すごく複雑そうにも見えます。しかし、実際にプログラマが記述すべきファイルは、基本的にはconfigure.acとMakefile.amの2種類だけです。その内容も、同機能のMakefileを自力で用意するのと比べると非常にシンプルです。もっとも簡単なMakefile.amの例としては、以下のたったの2行*2の記述だけで済みます。
bin_PROGRAMS = hello hello_SOURCES = hello.c
これだけで、後述する様々なmakeルールが生成されます。 依存ライブラリや対応プラットフォーム、ソフトウェアのオプション機能などが増えてくるとそれなりに面倒な記述は必要になりますが、Autotoolsを利用せずに自力でMakefileを用意する苦労に比べれば、やはりずっとシンプルに記述できます。
様々な環境に自分のソフトウェアを移植する必要がある場合、プログラマはそれぞれの環境の差異に頭を悩ませることが多々あります。
Autotoolsで生成されるconfigureは、こういったシステム間の差異を検出するためのスクリプトです。Autotoolsを使用することで、ソフトウェアを様々な環境に対応させるのが楽になります。
ただし、Autotoolsは移植性の高いプログラムを書くための補助をしてくれるだけに過ぎず、Autotoolsを使用するだけで移植性の高いプログラムができ上がるわけではありません。実際にプログラムの移植性を向上させるためには、プログラマがAutotoolsのコンセプトをよく理解し、そのことを常に意識しておく必要があります。
同等のツールの中では、Autotoolsは最も普及しているツールと言えるでしょう。普及しているため、Autotools対応ソフトウェアのビルド方法やその挙動は広く知られており、利用者は安心してビルドすることができます。また、それゆえにrpmやdebなどのバイナリパッケージを作成するのにも定番的な方法が用意されており、比較的容易にパッケージを作成することができます。
「高機能なMakefileが生成される」と述べたように、Autotoolsで生成されるMakefileには、単にソフトウェアをビルドする以外にも、色々と便利なmakeルールが自動で追加されます。以下にその一例を示します*3。
Autotoolsはインストールのためのmakeターゲット「install」を自動で生成するため、ビルドされた実行ファイルやライブラリなどはmake install
で適切なディレクトリにインストールすることができます。また、installと比べるとあまり知られていませんが、アンインストールのためのmakeターゲット「uninstall」も自動で生成されるため、一度インストールしたファイルを簡単に削除することもできます。
「clean」はmakeで生成されたファイルを削除するためのmakeターゲットです。「distclean」は「clean」に加えて、configureで生成されたファイルを削除するためのmakeターゲットです。Autotoolsは、明示的に削除するファイルを指定せずとも、自動的にこれらのmakeルールを生成します。もちろん、削除すべきファイルを手動で追加することもできます。
Autotoolsを使用するプロジェクトの多くは、tar.gzなどの形式でソースパッケージを配布しています。このソースパッケージは、make dist
で簡単に作成することができます。
単純にソースディレクトリをtarコマンドで固めればよいでは?と思う方もおられるかもしれません。しかし、開発者の手元のソースディレクトリ下には、ビルド時に生成されたファイルや、エディタが自動的にバックアップしたファイルなど、ソースパッケージには含めたくないファイルが多数存在しています。make dist
でソースパッケージを作成すると、これらの不要なファイルは除外され、ソースパッケージとして必要なファイルだけがパッケージ化されます。
make dist
が生成するデフォルトのパッケージ形式はtar.gzですが、その他の形式に変更することもできます。また、それぞれの形式のパッケージを作成するためのmakeターゲットも用意されています。
Autotoolsを使用すると、高機能かつ移植性の高いMakefileを容易に生成することができます。この便利なツールの使い方を覚えて、賢くソースパッケージを管理していきましょう。
*1 稀にAutotoolsを使用せずに独自に類似のスクリプトを用意しているソフトウェアも存在しますが、ここでは除外します。
*2 この例の場合はhello_SOURCESもAutotoolsで自動で補完できるため、実は1行で済みます。
*3 詳細はGNU Coding StandardのStandard Targetsなどを参照してください。
YARDというRuby用のドキュメンテーションツールがあります。APIのドキュメントの記述方法は大きく2種類ありますが、YARDはコードにコメントとしてドキュメントを埋め込む形式を採用しています。専用の記法を使って構造化された読みやすいドキュメントを書けることが類似ツールであるRDocとの大きな違いです。
今回は「Rubyで定義したメソッドの使用例を示す」ドキュメントのYARD流の書き方を紹介します。
なぜ使用例の書き方を説明するかというと、使用例を1つ示すだけで使い方をぐっとわかりやすく説明することができるからです。もちろん、引数や戻り値などメソッドについての情報も必要ですが、それらは断片的な情報なため、そこから全体像をイメージするにはもうひとステップ必要になります。一方、使用例は詳細を示すことには不向きですが、どんな状況で使うのか、どのように準備して使うのかといった前後関係も含めた全体像を示すことができます。
ライブラリーを初めて使うとき、最初にサンプルコードを動かして動作を確認した経験があるはずです。動作を確認し、サンプルコードの中の値を少しずつ変更して、実際に使うコードまで改良していったこともあるでしょう。このように、実際のコードで示された使い方はユーザーにとって有用な情報となります。
ユーザーではなく、ドキュメントを書く開発者の立場からも考えてみましょう。使用例を書くことで自分の作ったライブラリーのAPIが使いやすいかどうかを確認することができます。これは、よいソフトウェアを書くことに役立つことです。
このように、ドキュメントに使用例を書くことはユーザーにも開発者にもメリットがあります。
それでは、YARDで使用例を書く方法を次の順で説明します。
@example
タグの使用例を紹介@example
タグについて説明@example
タグを使って使用例を書き、その出力を確認@example
タグの使用例最初に使用例があるとわかりやすくなると説明したので、ここでも最初に@example
タグの使用例を示します。詳細は後から説明します。
@example
タグの同じ行に書いている「extract ages over 40」が使用例のタイトルで、それ以降のインデントされたコードが使用例になります。
1 2 3 4 5 6 7 8 |
# @example extract ages over 40 # overages = extract_overage([17, 43, 40, 56], 40) # puts("overages: #{overages.join(", ")}") #=> "overages: 43, 56" def extract_overage(ages, limit_age=20) ages.select do |number| number > limit_age end end |
どんな風に書くか雰囲気をつかめたでしょうか。
@example
タグについてそれでは、@example
タグについて説明します*1。@example
タグは、ドキュメント対象であるメソッドの使用例となるコードを示すために使います。後述するyardoc
コマンドを使って生成するHTMLのリファレンスマニュアルでは、コードが使用例だとわかるように整形されます。
@example
タグの書式は次の通りです。
@example 使用例のタイトル 使用例のコード
「使用例のタイトル」と「使用例のコード」に書く内容を説明します。
「使用例のタイトル」には、使用例を一言で示すような説明を書きます。@example
タグは複数個同時に使用できるため、それぞれの例が区別しやすくなるような説明になっているか確認してください。「この使用例が注目していることはなんだろう」と考えるとよいタイトルをつけられるはずです。
使用例のタイトルは省略可能ですが、できるだけ書くようにしましょう。タイトルをつけられない使用例は何に注目しているかが散漫になっている可能性が高いです。そのような使用例はユーザーにとってわかりにくいものです。「何に注目しているか」を忘れずにタイトルがつけられる使用例を書いてください。
「使用例のコード」には、使用例として示したいコードを書きます。コードは@example
タグの次の行以降に書きます。インデントすることを忘れないでください。インデントすることで、そのブロックにあるコードが使用例のコードであることを示します。
次に、実際に使用例を書くRubyのコードを示します。
@example
タグで使用例を書くコード(以降、「サンプルコード」と呼びます)は次の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 |
# Generates Array containing numbers exceeding limit_age. Numbers is members of ages. # For example, extract_overage([17, 43, 40, 56], 40) #=> [43, 56] # # @param ages [Array] numbers checked if they exceeded from limit_age. # @param limit_age [Integer] limit_age limit used to extract # bigger ages than it. # @return [Array] Returns Array containing numbers exceeding limit_age. def extract_overage(ages, limit_age=20) ages.select do |number| number > limit_age end end |
この状態のサンプルコードからHTMLのリファレンスマニュアルを生成してみましょう。これは、使用例を書いたサンプルコードから生成したリファレンスマニュアルと後で比較するためです。
HTMLのリファレンスマニュアルを作成するには、YARDに付属するyardoc
コマンドを使用します。サンプルコードをexample.rbというファイルに保存して、次のコマンドを実行してください。
% yardoc example.rb
yardoc
コマンドを実行すると、次のようなリファレンスマニュアルができます*2。
@example
タグの使い方いよいよ、@example
タグで例を書いていきます。
今回は使用例のタイトルに「extract ages over 40」を使い、使用例のコードは次のコードにします。
1 2 |
overages = extract_overage([17, 43, 40, 56], 40) puts("overages: #{overages.join(", ")}") #=> "overages: 43, 56" |
では、実際にサンプルコードのコメントに使用例を書きましょう。使用例の1つはすでにメソッドの全体の説明の中に「extract_overage([17, 43, 40, 56], 40) #=> [43, 56]
」と書かれていました。その例を@example
タグで書き直します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Generates Array containing numbers exceeding limit_age. Numbers is # members of ages. # # @example extract ages over 40 # overages = extract_overage([17, 43, 40, 56], 40) # puts("overages: #{overages.join(", ")}") #=> "overages: 43, 56" # # # @param ages [Array] numbers checked if they exceeded from limit_age. # @param limit_age [Integer] limit_age limit used to extract # bigger ages than it. # @return [Array] Returns Array containing numbers exceeding limit_age. def extract_overage(ages, limit_age=20) end </code></pre> |
コメント内で@example
タグを書く位置はどこでもよいのですが、メソッド全体の説明の直後、引数や戻り値の説明の前に書くことをオススメします。これは、ドキュメントを読む側の視点で考えるとわかります。引数や戻り値の説明の前に使用例を見ておくと、その後の引数や戻り値の説明を理解しやすくなるからです。使用例を頭に入れてから引数や戻り値の説明を見ると、使用例で使われていた引数や戻り値に説明を引きあわせて考えることができるため、使用例を見ていない状態よりも理解しやすくなります。
使用例を書いたサンプルコードからyardoc
コマンドを使ってリファレンスマニュアルを生成します。生成したリファレンスマニュアルは次のようになります。
HTMLのリファレンスマニュアルを見ると、赤枠の中に使用例のコードが表示されていることがわかります。
YARDでは@example
タグを使って使用例を書けることを実例を示しながら説明しました。また、HTMLのリファレンスマニュアルではどのように使用例が整形されるかも示しました。
今回は、YARDの書き方シリーズの4回目として@example
タグを使った使用例の書き方を紹介しました。使用例を書くことで、実装したメソッドを実際にどう使えばよいのかをわかりやすく伝えることができます。今後、ドキュメントを書くときは、ユーザーに使いやすさを伝えるために使用例も書いてみてください。
これまでも次の通りYARDの書き方を説明しました。こちらも合わせて確認してください。
リーダブルコードの解説やつくばインターンシップコンソーシアムのマッチングイベントをきっかけに、インターンシップへの応募が2件あり、今年2回目のインターンシップを9/2~9/6の1週間実施しました。インターンシップの記録はメモに残して公開しています。今回もインターンシップを通じてたくさんのことを学ぶことができました。
今回のインターンシップでは、2人のインターンがるりまプロジェクトにおける異なる2つの作業を行いました。1人はBitClustへの機能追加、もう1人はるりまのドキュメント改善です。一方のメンターも2名配置しました。1人はるりまプロジェクトのメンバーです。インターンへの作業内容の説明やインターンからの質問に対応しながら、るりまプロジェクトに関する作業も並行して行いました。もう1人は非エンジニアである筆者で、インターンシップで掲げたインターン、クリアコード双方の目的が達成できるよう調整する役を務めました。このような体制でインターンシップを実施したところ、インターンの作業が進まなかったり、違う方向に進んでしまうといった問題が発生しました。そこで今回はインターンシップで生じた問題と、それぞれの問題を解決するために試した工夫を紹介します。
インターンシップ開始当初、メンターがインターンに作業内容を説明した上で、インターンに作業を進めてもらい、疑問などが生じればメンターに相談してもらうことにしていました。しかし実際はインターンが質問することを遠慮したり、質問しないで自分で解決しようとした結果、相談する機会をもつことがあまりできませんでした。インターンが質問や相談することをトリガーとして問題を共有し解決しようとするやり方はうまくいきませんでした。しかしながら問題を解決するためには、まずインターンが抱えている問題を共有しなければなりません。そこで有効だったのが次に紹介する1日のふりかえりです。
1日のふりかえりは日々の作業状況と問題点をインターンとメンターが共有し、問題への解決策と翌日の作業内容を一緒に考えることを目的としていました。1日のふりかえりはクリアコードで行なっている1ヶ月のふりかえりを参考に、毎日夕方5時、1時間から1時間半をかけて行いました。
1日のふりかえりは以下の手順で行いました。
報告内容はホワイトボードに書き、それを見ながら話をしました。また、ふりかえりの内容は議事メモに残して、合意内容に認識のズレがないかメンバーで確認しました。
1日のふりかえりを行った結果、インターンが作業を進める上での問題点を洗い出し、解決策をみんなで考えることができました。問題から解決策を検討するには問題を整理しなければなりません。問題が生じた時のインターンの作業をたどって、どこでうまくいかなくなったか、またそのときどのような判断をしたのかをふりかえることで、問題が整理され、どこが原因だったかがわかりました。インターンシップの前半では問題への解決策はメンターが提案していましたが、後半になると問題を整理することを手伝えば、インターン自ら解決策を提案するようになりました。
1日のふりかえりで共有した問題に対して、解決策を決めて、翌日の作業で試してみました。ここからは、1日のふりかえりで見つかった問題に対する対応策で効果のあったものをいくつか紹介します。
インターンシップの1日目、2日目と予定通りに作業が進みませんでした。作業完了に必要だと見積もった時間が適切でなかったことが原因の1つでした。作業時間を見積もることは経験のない人にとって困難なことです。同じ作業を繰り返し行うのであれば見積もることもできるようになりますが、インターンシップでは次々と新しいことに取り組むので、経験のないインターンが作業時間を見積もることはできませんでした。そこでメンターがこの作業なら3時間もあればできるだろうと判断し、3時間で作業を完了させるよう指示していました。しかし、結果は3時間経過しても完了せず、インターンは作業が完了するまで作業を続けていました。 メンターが見積もった時間はあくまで目安でしかなく、インターンが作業を完了させるのに必要な時間を適切に見積もったとはいえませんでした。メンターがインターンの作業時間を見積もることも難しいことがわかりました。そこで解決策として、作業時間を決めて、その作業時間が経過した時点でインターンがメンターに作業状況を報告することにしました。これによって、インターンが予定時間を超えて作業を続けることはなくなりました。またその時点で作業が完了していない場合は、作業状況をメンターが確認し、作業完了に向けてどう進めていくか相談することができました。
作業着手時には想定していなかった問題が発生し、その問題に対処するためにどうするかインターンが1人で長時間悩むことがありました。相談する前に、自分で調べられるところは何とかしようという意識が強かったこともあり、なかなか相談できなかったようです。 そこで、悩んでいる時間を制限するため、悩んでいる時間が一定時間を超えたらメンターに相談することにしました。前述の時間の区切りがだいたい1時間だったので、15分に設定しました。その結果、インターンからメンターへの相談が増え、悩んで手が止まる時間は短くなりました。メンターもインターンの手が止まっていれば、早めに声を掛けるようになりました。
今回はインターンシップで学んだことから、インターンの作業を進めやすくするための工夫を紹介しました。こまめに状況を共有し、問題を一緒に解決するような仕組みを用意することによって、インターンの作業が進めやすくなりました。また経験の少ないインターンにとって、相談することは難しく、いいアドバイスをもらうための相談の仕方にあるように準備をして相談できるようになるためには練習が必要です。1日のふりかえりや相談を増やす取り組みは、相談するときの手順にある今の状態と目指している状態を整理する機会になりました。これらの工夫はインターンシップだけでなく、新しい社員を迎えるときにも活用できそうです。