みなさんこんにちは。早いもので今年も終わってしまいますね。1年いかがお過ごしでしたでしょうか。私は引きこもりすぎて世間のクリスマスの空気に触れることもなくクリスマス当日になってしまい、既に街中は正月準備一色となっていました。無念…………。
TEXT_痴山紘史 / Hiroshi Chiyama(日本CGサービス)
EDIT_尾形美幸 / Miyuki Ogata(CGWORLD)
テストを書く
これまでの記事の中で、「テストコードを書く」ということについて何度か触れてきました。私が確認したところ、第33回、第36回でテストについて言及していました。ただ、テストを書き慣れていない人が “テストを書きましょう” と言われても、漠然としすぎて何から手を付けたら良いのかわからないと思います。また、なぜテストを書くのかという理由がわからなければ、テストを書く気にもならないです。そこで、今回はテストを書くことの理由、テストを書く際に気を付けることについて考えていきます。
まずはテストを書く理由について考えてみます。
生産性の維持
テストを書くということは、処理を行うプログラム本体に加え、そのプログラムが正しく動くか確認するためのプログラムを書くということを意味します。プログラムを書く量は当然増えているので、それだけを見ると開発の効率は落ちています。
それでもテストを書く理由は、開発を続けたときの生産性の低下を極力抑えるためです。
テストを書かないで開発していると、最初は開発に専念できるので生産性が高いです。しかし、たび重なる仕様変更やコードの複雑化などによってプログラムに変更を加えるたびに確認すべき事項がどんどん増えていき、変更が思わぬところに影響をおよぼす事態も増えていきます。こうなると、どんどんバグ修正や動作確認に労力を割かれるようになってしまいます。
これに対し、テストが用意されていれば、テストを実行することで少なくともテストでわかっている範囲ではプログラムが壊れていない、もしくはどこが壊れたかを確認できます。この安心感によって大幅な修正も怖くなくなるため、より良いコードにするためのモチベーションを保つことができます。これらの要因によって、開発を続けた際の生産性の低下を抑えることができます。そして、開発の比較的早い段階で両者の生産性が逆転します。
また、生産性の維持と同等以上に、プログラムに対して変更を加えるときの安心感、ひいては心の安定を保てることは、開発者にとって大きな価値があります。
最初のユーザーになる
テストを書くと、足りない機能や引数のルールが揃っていない関数など、元のプログラムのイケてない部分が見えてきます。これには、作成したプログラムを使用してテストを書くことで、ユーザーの目線で見られるようになることが大きく影響しています。プログラムを書いているだけでは意外とこの目線が抜け落ちてしまい、実際に使用する段階になってイマイチ使いづらいことに気付くということが起きがちです。
テストを書くことで自分がプログラムの最初のユーザーになれるため、より良い設計をするための動機が生まれます。
初めてテストを書く
効果がありそうなことはわかった。でもどうやってテストを書くの? という疑問が残ります。具体的な内容については、次回ご紹介します。
まずはテストを書くことに対する心理的なハードルを下げることが大事なので、どんなにくだらなくて当たり前だと思えるようなことでも良いので、テストを書いてみることをオススメします。
例えば、第37回で作成したMayaを起動してabcファイルを生成する関数をつくったとします。
def _launch_maya_and_convert_to_abc(src, dst):
# 何やかんやいろいろがんばってmaファイルをabcファイルに変換する
return True
この関数では、少なくとも
・正常終了したらTrueが返ってくる
・dstにファイルができている
という2つのことがわかっています。これをそのままテストとして書けば良いのです。Pythonのunittestを使用すると、以下のようになります。
import os
import unittest
class Test_Convert(unittest.TestCase):
def test_launch_maya_and_convert_to_abc(self):
src = 'PATH/TO/FILE.ma'
dst = 'PATH/TO/FILE.abc'
# 関数を実行する
result = _launch_maya_and_convert_to_abc(src, dst)
# 実行した結果、こうなるはずとわかっていることを書く
self.assertEqual(result, True)
self.assertEqual(os.path.exists(dst, True)
test_ほげほげ という名前のついている関数がテスト本体です。どうでしょう? 拍子抜けするほど単純ですね。これでも、何かの理由で変換に失敗するケースを検出できますし、関数が正常終了しているのに .abcファイルができていないという問題も検出できます。
リファクタリングしながらテストを書く
テストを書くタイミングもなかなか難しいです。いきなりコードがない状態からテストを書くのは難しいですし、かと言って既に動いているプログラムに対してテストを書こうにも、どこから手を付けたら良いか途方に暮れてしまいがちです。特に中がグチャグチャになってしまったコードの場合は大変です。
こういう場合、リファクタリングをする部分が正常に動いていることを確認するテストを書いてからリファクタリングをして、リファクタリング後に事前に書いたテストを用いてプログラムが壊れていないことを確認するという方法をとるのが良いです。
…………とは言うものの、これも最初はけっこう難しいです。
リファクタリング前のコードがそもそもテストを書けるほど整理されているかという問題もありますし、リファクタリング前後で構造が変わってしまい、せっかく書いたテストがそのまま実行できないという問題も起こり得ます。もちろん、変わった部分を確認しながら手直ししていく工程もとても大事なのですが、テストを初めて書く段階でそこまでやるのはハードルが高いです。そのため、最初はリファクタリング後にテストを用意して、少なくとも新しいコードが想定した通りに動いていることを確認するのでも十分だと思います。
検体を採集しておく
バグが発覚したときに、そこで使用していたデータや状況を保存しておき、バグフィックスする際にテストを書いて動作確認するという習慣を身に付けると、とても良いです。こうすれば、少なくとも一度発生したバグは二度と発生しなくなります。バグを修正したら必ず動作確認は行うので、そのついでにテストコードという資産を蓄積できるのは一石二鳥です。
テストと相性の悪いものたち
テストは万全ではなく、内容によってはテストと相性の悪いものもあります。その例をいくつか挙げてみます。
データベースに依存したテスト
データベースがからむと非常に厄介です。データベースがからむ場合、バグが発生するための条件が複雑であったり、様々な前提条件を満たした際に起こるなど、事前のデータ準備が必要なことが多く、テストのためのデータを作成する部分にも前提知識が必要になってきます。
そうなると、データベースの仕様が変わったときにどのようになるのが正しいのか、テストコードの歴史を紐解きながら判別する必要が出てきます。これはとても大変で、メンテナンスが億劫になりがちです。
GUIにからんだテスト
GUIとテストがからむと状況はかなり厳しくなります。最終的には実際に操作してテストすることが必要になるものの、GUIの内容を前提としたテストコードはGUIが少し変わっただけで使いものにならなくなる可能性が高いです。
複雑な動作をするGUIに関してはさらに悪夢で、GUIの挙動全てをテストコードに落とし込むための労力が段ちがいにかかる上、GUIの挙動が変わったら大幅な書き換えが必要になります。ここで問題になるのが、GUIを変更するとテストを書き直すためのコストがかかるため、GUIの改良に対する心理的なハードルが大幅に高くなってしまうことです。特に、一番テストがほしい、試行錯誤しながら開発している段階でこのコストがかかってしまうのはかなり厳しいです。
また、GUIの場合は処理結果の判定が一筋縄でいかない場合も多々あります。場合によっては画像処理をして判別することも必要になるでしょう。これをやるのはかなりコストがかかるのでつらいです。
以上のような理由から、私はGUIに対してテストコードを書くのはほぼ諦めています。テストが必要な場合、そのコードは使い捨てと割り切って極力低コストで用意するようにします。その代わり、GUIの挙動を極力シンプルにすることと、処理本体とGUIを分離して、処理に対するテストを書きやすくすることを心がけています。
テストも足枷になる
テストを充実させることも大事ですが、無計画にテストを増やすのは得策とは言えません。本質的でないテストが増えてしまうと、ちょっとした仕様変更のたびに大量のテストで失敗が出るようになり、その修正のコストが重くのしかかってくることにもつながります。これでは本末転倒です。また、テストが増えればそのぶんだけテストを実行するための時間も増えます。これはリリースのためのコストに直接反映されるため、注意が必要です。
過去の経験として、一度コードをコミットするとテストを実行するのに30分ほどかかるようになってしまったことがありました。これでは1日の作業時間を8時間とすると、最大で16回しかテストを完遂できないことになります。また、コミットしてから30分待たないと結果がわからないというのもとても効率が悪いです。
もう一歩先に : TDD
テストを前面にもってきたTDD(Test Driven Development)というものが存在します。これまではプログラムを最初に書いて、それに対してリファクタリングや計測、テストを整備するというながれだったものを、最初にテストを書き、そのテストを通すためのプログラムを書いていくというのがTDDになります。
TDDは非常に強力な手法で、これが徹底できればとても素晴らしい開発体制を構築できます。ただ、これを常に実践できるほど人間の心は強くないという問題があります。やってみるとわかるのですが、目の前に問題があってその解き方がわかっているときに、まずはテストを書くというのは正直つらいのです……。また、TDDのリズムにのっているときは良いのですが、ちょっと間が空いたりしてリズムが崩れると習慣を取り戻すのが難しいです。ここは上手く折り合いをつけながら、無理のない範囲で実践していく、くらいが良いのかなと思います。
まとめ
テストは突き進めていくとかなり奥が深く、さらに挫折する要因がそこら中に転がっているため、なかなか習慣付けるのが難しいです。私なんかは思い付いたらパッションのおもむくままにガガガーッとコードを書いてしまうことが多いため、テストを整備することとの相性もあまり良くなかったりします。本当は良くないんですけどね。
まずはあまりストイックにならず、やらないよりはやった方がマシの精神で気が付いたところから少しずつテストコードをストックしていき、少なくともストックした部分に関しては、いつでも簡単にプログラムのテストを実行できるようにしていくことから始めるのが挫折を避けるコツだと思います。
第41回の公開は、2022年1月を予定しております。
プロフィール
痴山紘史
日本CGサービス(JCGS) 代表
大学卒業後、株式会社IMAGICA入社。放送局向けリアルタイムCGシステムの構築・運用に携わる。その後、株式会社リンクス・デジワークスにて映画・ゲームなどの映像制作に携わる。2010年独立、現職。映像制作プロダクション向けのパイプラインの開発と提供を行なっている。新人パパ。娘かわいい。