意識が高くないVisualStudioを使用した単体テストの自動化

Table of Content

はじめに

この記事ではVisualStudioを使用して、どのように単体テストを自動化していくかを記述します。

この記事では「単体テストの自動化?なにそれおいしいの?」とかいう感じの組織で、現実的な妥協点を探っていくという、縛りゲーをやるクソゲーマー向けて、ファミ通程度の攻略情報をお届けするのが目的です。

あと「単体テスト」を自動でやるという枝葉の話でしかないので、「テスト?なにそれおいしいの?」とか「品質管理?わいには関係ないで」とかいう方は「自動」云々でなく、まず基本的なソフトウェア技法から調べた方がよろしいです。

「まっとうなソフトウェアテストの知識」を持っている人が、ツールを駆使して、作業の効率化をもとめ品質を上げるってのが重要で、「まっとうなソフトウェアテストの知識」がない人が、手順だけを自動化するのは本質を踏み外しています。

JSTQBの教科書とか、ソフトウェアテストの本とかでもアマゾンで買ったほうが有益でしょう。

自動化を目的にするのはやめましょう。

単体テストの自動化の目的

「単体テスト」をテストコードによって実現します。
流れとしては以下の全てをテストコードにより実現します。

・テスト対象が「どこ」でも「何時」でも実行できるように実行できる前提を作る
・テスト対象を実行する
・テスト対象が正しく動作したか検証する

これにより、回帰テストの効率が高まります。
たとえば、別の機能を修正した場合、すくなくともテストコードが記述している箇所の挙動は保障されます。
また、人力で実行した場合、人間は疲労等で結果の判定のミスをします。
手動の単体テストで合格していたはずの項目があとになって動いてなかった経験はいくらでもあるでしょう。すくなくとも、テストコードは何回実行しても、書かれた通りにしか動かないので、疲れてミスもしませんし、証跡として無駄なスクリーンキャプチャーをとるより効率的ですし、意味もあります。

しかし、あくまで単体テストです。結合テストは別に考えましょう。
単体テストを自動化して完全に動いても、それによって品質が高いと等価にはなりません。
テストコードを書くことは重要ですが、それに過度の期待を持つべきではないというのも重要です。

たとえば画面の使い勝手や、同時実効性の問題は単体テストで検出するのはかなり厳しいです。

この前提は管理する側含めて全員の共有にした方が良いです。
「単体テストを自動化したから結合テストの工数を削っていいよね」とかいう楽観主義は防ぐようにしてください。
特に前線から離れた管理職は夢見がちな都合のいい楽観主義を現実と思い込む傾向があるので、ここは釘を強くさしておいてください。

「単体テストはあくまで単体テスト。結合テストは別で考えろ」

テスト対象が「どこ」でも「何時」でも実行できるように実行できる前提を作る

作成した単体テストが「どこ」でも「何時」でも実行できるようにするというのが重要です。
たとえば、現在日付に依存するようなテストや、テストコードを書いた担当者の端末でしか動作しないようなテストは負債になります。

この工程では、それを防ぐために現在日付を取得する関数が常に同じ日付を返すように調整したり、どこの端末でもテスト対象が動作するように、ファイルやデータを読み込めるようにしたりします。

つまり、テスト対象が依存している機能がテストに都合のいい動作をするように設定します。
VisualStudioの場合は、MoqやMicrosoftFakesを使用して、依存した機能をテストに都合のいいように設定するとよろしいでしょう。

MSTestによるユニットテストの自動化 - Moq
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24#moq

Microsoft Fakesを利用したテストコードの記述
http://qiita.com/mima_ita/items/9ebb0f40d3209f33a45d

また、よくあることですが、ある特定のテストを実行した後だと動かないということが多々あります。
この工程では、前後で如何なるテストが行われても動くような状況を作成してください。

テスト対象を実行する

GUIのテスト対象を実行するのは前段階が色々困難ですが、単体テストの場合、そのテスト対象の関数のみを実行すればいいのでなんとでもなります。

特に.NETの場合はリフレクションが使用できるので、プライベートメソッドだろが、プライベートプロパティだろうがアクセスすることが可能です。

VisualStudioのProfessional以上の場合、テストコードを記述するプロジェクトが作成できるので以下を参考に対象の関数を実行するといいでしょう。
プライベートへのアクセスはPrivateObjectを使用してもいいですし、リフレクションを使用してもいいでしょう。

MSTestによるユニットテストの自動化
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24

C# で private メソッドを呼んでみる
http://normalian.hatenablog.com/entry/20090124/1232782230

テスト対象が正しく動作したか検証する

テスト対象が正しく動作したかの検証も重要です。
良く見るのが、テストの実行まではテストコードに書いて、実行結果を目視で確認するというやり方です。
これは古式ゆかしいデバッグ方法ですが、自動化とはいいません。

MSTestには様々なAssert関数が用意されているので、それらを使用して実行結果を検証しましょう。

様々なAssert機能
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24#%E6%A7%98%E3%80%85%E3%81%AAassert%E6%A9%9F%E8%83%BD

また、オブジェクトの内容をチェックする際は、それぞれのプロパティをAssertで愚直チェックをしてもいいですが、オブジェクトをJSONに変換してチェックする手もあります。

        public class TestData12
        {
            public class TestDataItem
            {
                public int key { set; get; }
                public string name { set; get; }
            }

            public string GroupName { set; get; }
            public List<TestDataItem> Items { set; get; }
        }

        [TestMethod]
        public void TestMethod12()
        {
            var expData = new TestData12();
            expData.GroupName = "Gp1";
            expData.Items = new List<TestData12.TestDataItem>();
            expData.Items.Add(new TestData12.TestDataItem
            {
                key = 1111,
                name = "item1"
            });
            expData.Items.Add(new TestData12.TestDataItem
            {
                key = 2222,
                name = "item1"
            });
            string json = Newtonsoft.Json.JsonConvert.SerializeObject(expData);
            Assert.AreEqual(
                "{\"GroupName\":\"Gp1\",\"Items\":[{\"key\":1111,\"name\":\"item1\"},{\"key\":2222,\"name\":\"item1\"}]}",
                json
            );
        }

これを実行するにはNewtonsoft.Jsonが必要になります。
http://www.newtonsoft.com/json

ここで紹介した方法はあくまで基本です。
個々のプロジェクト用に必要な検証用のユーティリティ関数を作成するといいでしょう。

どれを自動化すべきか?

意識が高い系の記事なら、「全部、カバレッジ100%に決まっているだろ」とか言いだしますが、ここでは「工数と開発者のスキルしだい」という面白くもない解答になります。

単体テストの自動化自体がはじめての場合

単体テストの自動化が初めての場合は、簡単にできて効果の高い箇所をテストするといいでしょう。
これは他のクラスになるべく依存していない、ユーティリティ関数のテストです。

static classの中から、なるべく多く機能で使用されているところから選定するといいでしょう。

共通的な機能あるいは業務的に重要な機能

次に単体テストを自動化して効果のある機能は多くの機能から呼ばれている機能です。
多くのクラスのベースとなるクラスや、多くのクラスから参照される機能が対象になるでしょう。

単体テストを自動化することにより、仮にその共通機能に修正が入っても、他の機能に影響を与えないような修正になっていることを保証する回帰試験が容易になります。

また、品質の確保においてシビアなものを要求される重要な機能においても単体テストを自動化するのは有効です。

テストコードで保障されていない箇所を調べてテストコードを書く

VisualStudo2013のPremium以上もしくはVisualStudio2015のEnterpriseのエディションにはコードカバレッジを計測する機能がついています。
これにより、どの機能がテストコードにより実行されていないか調べることが可能です。

(1)[テスト]→[コードカバレッジの分析]→[すべてのテスト]を選択
vs002.png

(2)コードカバレッジの結果が以下のように表示される。
vs003.png

関数単位で未カバー(実行していない)箇所が出力される。

(3)結果の関数をクリックすると実行済みの行は青、未実行の行は赤で表示される。
vs004.png

これらの機能を利用して、未実行の行を探して、そこを通るようにテストコードを書いていきます。

品質が悪そうな個所を探してテストコードを書く

ある程度、経験を積むとマズイ書き方をしているコードはわかります。
たとえば、以下のようなコードはヤバい臭いがぷんぷんとします。

for(int i = 0; i < x.Count;++i)
{
  for(int j = 0; j < y.Count;++j)
  {
    if(x[i].name == "XXX")
    {
       if (y[j].name == "ZZZ")
       {
          hoge1();
          if (hoge2() == 3)
          {
             hoge4();
          }
          else
          {
            if (hoge3() == 3)
            {
               if (hoge4() == 5)
               {
                  hoge6();
               }
               else if (a==5)
               {
          if(x)
                 {
                 }
                 else
                 {
                 }
               }
            }
          }
          hoge5();
       }
       else if (y[j].name = "dddd")
       {
          hoge1();
          if (hoge2() == 3)
          {
             hoge4();
          }
          else
          {
            if (hoge3() == 3)
            {
               if (hoge4() == 5)
               {
                  hoge6();
               }
               else if (a==5)
               {
               }
            }
          }
          hoge5();
       }
    }
  }
}

しかしながら、「クソコードだから直せよ、凸助」というと刃傷沙汰になります。
そういう、マズイ書き方をしている箇所を定量的に見つける手段として、「サイクロマティック複雑度」という指標が存在します。

循環的複雑度(Cyclomatic complexity)
https://ja.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E7%9A%84%E8%A4%87%E9%9B%91%E5%BA%A6

簡単にいうと、ループや分岐が多いほどこの複雑度は高くなります。
1関数で25を超える場合、複雑度が極めて大きく、バグが潜んでいる可能性が高いです。

VisualStudioでは以下のようにサイクロマティック複雑度を計測します。

(1)[分析]→[ソリューションのコードメトリックスを計算]
vs005.png

(2)コードメトリックスの結果が表示される。
vs006.png

サイクロマティック複雑度は25未満に抑えるように実装するのが筋ですが、直せない場合は、これを超える箇所を集中的にテストコードを記述しましょう。
(だいたいバグがでてくるはず)

自動テストはいつ書いて実行すべきか?

実装しながらテストコードを書いたほうが楽です。
すくなくともテストコードが書けるレベルのコードになります。
そしてテストコードは最低毎日実行して、必ず全てのテストコードが合格するように監視してください。

人間は安易な方向に、あっさり流されます。

いくら、「どの環境でもテストが動くようにしろ」と言っても自分の環境でしか動かないテストコードはコミットされますし、「テストコードを動かして合格してからコミットしろ」といってもテストを不合格にするような安易な修正がコミットされます。

これはなにをどうやっても防げません。

そしてそれを放置すると、動かないテストが大量に作成されて、単体テストの自動化が形骸化します。最悪[Ignore]属性を付与して無視する形でいいので、テストが合格しつづけることだけは死守しましょう。

「毎日テストを実行して、その結果が合格になるように維持し続ける」ということだけは、なにがなんでも死守してください。

コマンドラインからビルドしてテストを実行する方法

毎日実行しつづけるのに手でやるのはしんどいです。
なので、コマンドラインから実行できるようにして、タスクスケジューラーかなにかで毎日実行するといいでしょう。

(1)MSBuildを使用してソリューションをビルドする

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild TestSample.sln

(2)コマンドプロンプトからテストを実行する。

# 環境変数の設定
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"

# テストの実行
vstest.console.exe UnitTestProject1\bin\Debug\UnitTestProject1.dll

コマンド ラインからの VSTest.console の使用
https://msdn.microsoft.com/ja-jp/library/jj155800.aspx

可能ならJenkinsを使用するといいですが、手段は問わないので毎日実行して結果を監視しましょう。

どう説得するか?

テストコードを書くことを認めさせる方法としていくつかのケースが考えられます。
ここでは、その説得方法を考えてみましょう。

テストコード書かないと単体テストができないケース

このパターンが一番説得しやすいです。
なぜならば、手動でテストができず、テストコードでしかテストできないので反論がしようがありません。
代替手段のないことを提案するのが一番楽です。

外部の機能に依存しており、それが完成していない場合

たとえば、外部の機能に依存しており、そのシステムが現在開発中で手動でテストできないケースを考えてみましょう。

従来であれば、結合テストまで先送りして地獄を味わいます。
しかし、テストコードを書くならば、外部のシステムのインターフェイスをMoqまたはMicrosfotFakesで偽装して単体テストを行うことが可能になります。

結合テストでのリスクを事前に減らせるというのは大きな説得材料になるでしょう。

大量のパターンがある場合

たとえばある関数の引数が取りうるパターンが数百、数千あったとします。
これは手動で実行するのは無理です。

しかしながら、テストコードであれば、パラメータを機械的に作成してテストコードを実行するだけなので有効です。

関連する機能、もしくは自機能に変更が予測されテストを何回も行う必要があるケース

次善のパターンです。
このケースをもって説得する場合、以下の工数の勝負になります。

手動テストの工数:「一回の手動テストに係る工数」×「予測される変更回数」
自動テストの工数:「テストコードを記述する工数」

これを算出して金額の差額で、勝負しましょう。

品質で勝負するケース

これは分の悪い勝負です。

テストコードを書いた方が品質が良くなる傾向がありますが、それを費用体効果で表すのはかなり難しいですし、事前に説得する材料を持ってくるのは無理でしょう。

ここで勝負するには、テストコードを書いた機能と書かなかった機能でバグ数/step数で比較することになりますが、この時点で手遅れなので、一手遅れます。

FAQ

よくある質問をまとめます。

他の開発者がテストコードを書いてくれません。

作業者向けの答え

そういうもんです。あきらめましょう。
無理やり書かせても、やっつけ仕事で意味のない仕事をあげてくるだけです。

ただし、他人が書かないのは、貴方が書かない理由にはなりません。
少なくとも貴方は書けばいいです。
貴方が書き続ける限りいずれ、カバレッジは100%に近づきます。

管理者向けの答え

泣き言いっていないで、テストコードを書ける人間を調達するか、教育しましょう。
学習期間を用意する、テストコードを書くための工数を用意する、テストコードが正しくかけたかチェックする工数を確保するのは管理職の職責です。

管理者がテストコードを書く工数を確保してくれません。

作業者向けの答え

そういうもんです。あきらめましょう。
本当に、そのテストコードが必要だと思うなら作業進捗を過小報告して裏で書きましょう。

残念ながら、まっとうじゃない状況で、まっとうなことをしたかったら、まっとうじゃない手段をとるしかありません。

管理者向けの答え

確保しましょう。
どうしても確保できんのなら、優先度を考えて効果の高い部分だけ適用するのはどうでしょうか?

それでも私は周りにテストコードを書かせたいのです。

作業者向けの答え

権限のない人間が他人の行動を決めることはできません。

テストコードを書かない機能にバグがないなら、それはそれでうまくいっているので、無理にうまくいっている事を変更する必要がないでしょう。わりきりましょう。

バグが大量にあるなら、バグ報告をして、「品質に問題があるので是正処置をとる必要があります」という方向にもっていきましょう。

この際、テストコードがある機能のバグ件数/Step数と比較してやれば、管理職はテストコードを書かないとまずいんじゃないかと思い始めます。
その事実を10か20も積み重ねれば、組織としてテストコードを書くようになるか、テストコードを書ける奴を雇ってくるでしょう。

管理者向けの答え

こんなもん読んでないで、さっさと人を教育するか適切な人雇う手筈を考えた方がいいでしょう。
貴方の組織の問題は貴方でないと解決できません。

テストコード書いてくれるのですが、合格しないコードをコミットしやがります

作業者向けの答え

そういうもんです。あきらめましょう。

毎日、全体メールで「偉い管理職」でもまぜて不合格の旨を通知しつづけましょう。
そのうち折れて直してきます。直さなくても管理職が問題視して勝手に対応してくれます。

放置されるようなら徐々に上役をCCにまぜていくといいでしょう。最後の偉い人まで行って放置されるなら、その組織の命数はもう長くないので、「プログラマがプロジェクトを救おうなどとおこがましいとは思わんかね?」とでも、ご自身に言い聞かせて、辞める準備をしましょう。

管理者向けの答え

テストが通らないコードはコミットさせないという運用ルールを作って周知します。

可能なら、テストコードが適切に動作しているかチェックする人間を任命しましょう。
当然、通常の作業量を減らすべきです。

テストコードが書きずらいです。

作業者向けの答え

GUI、マルチスレッドなどはたしかにテストコードは書きずらいので、そういう単体テストに向かないところはテストコードを書かないのも手です。

そういう理由がないのに、テストコードが書きずらいのは単純に実装がへたくそなだけです。
自分が書いた実装ならテストが書けるように直しましょう。

他人が書いた実装なら、バグを淡々と報告して、「作り直した方がはやい」ということを上に理解させましょう。
たぶん、そういう実装を仕上げてくる方の成果物の品質は極めて悪いので、簡単にバグを見つけることができます。

管理職向けの答え

特別の理由がない場合、実装が不味い可能性があります。
サイクロマティック複雑度の計測をしてみてください。
異常な値が出た場合は、残念ながらその機能はバグを含んでいる可能性が極めて高いです。
別の人間にプロダクトコードの修正をできる権限をあたえて見直しをさせた方がいいでしょう。

単体テストを自動化する意味がわかりません

作業者向けの答え

作りっぱなしで、終われる仕事だけしていればいいというなら、その考えもよろしいんじゃないでしょうか。

管理職向けの答え

しょせん、コストと効果のトレードオフにしかすぎません。
変更が発生する頻度と1回のテストの手間で、解答がかわります。
プロジェクトの性質によっては、その答えになるのも已む得ないでしょう。

ただし、もし、あらゆる状況で意味がないというお考えならば、そもそも「ソフトウェア」の管理には向いていない疑義があります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です