この物語はフィクションです。そういうことにしておいてください。
こんにちは。Java訓練中の川原です。
テストがない。
そんな現実のレガシーコードと戦う事になったとします。
テストがないコードというのは、テストを実行することを考えて作られていないので、いざテストを書こうとすると(僕の技量がまだまだなことも大きいが)怖気づいてしまい、結局またテストが書けないという悪循環に陥る可能性があります。
改修するタイミングなどに、思い切って改修する場所の周辺だけでもテストを作っておきたいものです。
勇気必要ですよね。ケント・ベック本の「まえがき」にもあるとおりです。
こんなケースに出会った
今回は、 Java のケースを想定してください。
Controller.execute() の改修を行うので 、命綱として Controller のテストを書いておきたいとします。
名前の通り、 Singleton は、 Singleton Pattern を実装したクラスです。 例えば、データベースの接続状態などを管理し、データを取得するような機能を持つとかそういうのを想定してください。
Util はクラスメソッドをもっています。やはり、こちらも外部コンテンツに接続した結果が帰ってくるようなものを想定してください。
おまけに、 Controller には、 fetchData() という、これまた何が帰ってくるかわからないようなメソッドがあったと想定します。
Singleton.foo() 、 Util.bar() そして、 Controller.fetchData() は、テストとしてはなかなか扱いづらいです。しかも、ものすごくたくさんの場所で使われているとして、他のクラスがまったくテストを書いていないとすればいきなり全力でゴリゴリとリファクタリングしまくるのも辛いわけです。
(時間ないなら尚更辛い!)
そんな時に、Mock を書くことになるかと思いますが JMock では苦しい気がしました。なぜなら、JMock では Mock として利用されるインスタンスは、インターフェイス化されていないといけないからです。
また、 Util.bar() のようなクラスメソッドをモックする機能は JMock にはありません。
Java には、 Mockito という Mocking Framework もありますが、やはりクラスメソッドのモック化は厳しいような雰囲気でした。
というわけで、 現実と戦うためには JMockit が最強っぽい予感です。
たのしいモック
// Controller.java package net.bucyou; import org.apache.commons.lang3.RandomStringUtils; public class Controller { public String execute() { return fetchData() + Singleton.getInstance().foo() + Util.bar(); } public String fetchData() { return RandomStringUtils.randomAscii(2); } }
// Singleton.java package net.bucyou; import org.apache.commons.lang3.RandomStringUtils; public class Singleton { private static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (null == Singleton.singleton) { Singleton.singleton = new Singleton(); } return Singleton.singleton; } public String foo() { return RandomStringUtils.randomAscii(2); } }
// Util.java package net.bucyou; import org.apache.commons.lang3.RandomStringUtils; public class Util { public static String bar() { return RandomStringUtils.randomAscii(2); } }
さて、厄介なコードを用意しました。
RandomStringUtils.randomAscii(2); に関しては、ランダムで2文字のAscii文字を返します。
当然、 Controller.execute() した結果として、何が帰ってくるかはわかりません。
// ControllerTest.java package net.bucyou; import static org.junit.Assert.*; import static org.hamcrest.core.Is.is; import org.junit.Test; public class ControllerTest { @Test public void testExecute() { Controller controller = new Controller(); assertThat(controller.execute(), is("AABBCC")); } }
とりあえず書いたテストです。当然失敗です。
さて、モックを作る準備として、 jmockit.jar をビルドパスに組み込みます。
ドキュメントによると、 classpath の順序に注意らしいです。JUnit の前に持って行きましょう。
さらに、テスト実行時に -javaagent:jmockit.jar などと、 VM argument に追加してやる必要があるそうです。
(ココらへんが、どういう理由で必要なのかはまだちゃんと理解してないです。)
さぁ、モックを書こう!
// ControllerTest.java package net.bucyou; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import mockit.Expectations; import mockit.Mocked; import org.junit.Test; public class ControllerTest { @Mocked final Util util = null; @Test public void testExecute() { final Controller controller = new Controller(); // Expections は呼ばれる順番もチェックするぞ! new Expectations(controller) { Singleton singleton; { // controller.fetchData() のパーシャルモック // このメソッドだけ挙動変更とかができる。 controller.fetchData(); result = "AA"; // Singleton のモック Singleton.getInstance(); result = singleton; singleton.foo(); result = "BB"; // Util のモック Util.bar(); result = "CC"; } }; assertThat(controller.execute(), is("AABBCC")); } }
そして、実行。
ハハハ。美しい Test 成功のグリーンが輝いておる。
これでできた!
やったね!これで、テストを足掛かりに少しは前進することができるかなー。とか考えたわけです。
あと、モックを作るのは楽しいです!
JMockit は、カバレッジ計測を行ったりもできるらしいので、リア充な Mocking Framework だと思います。リア充乙。
ちなみに、これなんて読むんですか。
ジェイモキットとか読んでるんですけど。 JMock + Mockito で、ジェイモヒートとかなんですかね?