かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot でメール送信する Web アプリケーションを作る ( その17 )( モックツール JMockit を試す )

概要

Spring Boot でメール送信する Web アプリケーションを作る ( その16 )( 日本語のファイル名の添付ファイル付メールを送信する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
  • 以前購入した「JUnit実践入門」にモックに関する記述があり、モック用ライブラリでモックを作成することで呼び出す先のクラスやメソッドが未実装の時にもテストが出来るという点に興味がわきました。使い方等を調べてみたところ、いろいろな人の Blog で JMockit というモック用ライブラリが最強と言われていたので、試してみたいと思います。

    JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

    JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

  • 少し前まではテストを書くのは面倒だなと思っていましたが、いろいろ調べてみると Java のテストライブラリは便利で使いやすいものが揃っており、テストを書いて動作確認をしないなんてあり得ないな、と最近強く感じています。

ソフトウェア一覧

参考にしたサイト

  1. JMockit
    http://jmockit.org/

  2. IntelliJ IDEAでjarファイルのクラスパスの順番を変更する
    http://shinodogg.com/?p=4799

  3. 最強モックツール JMockit その1
    http://genesis-tdsg.blogspot.jp/2013/08/jmockit.html

    • 株式会社ジェニシス技術開発事業部様の blog です。その1~その13まであります。JMockit でできることを理解するのに一番わかりやすかったです。まとまった情報を公開していただいて感謝しています。
  4. 最強モックツール JMockit その2 インストール編
    http://genesis-tdsg.blogspot.jp/2013/08/jmockit_25.html

  5. 最強モックツール JMockit その3 Webリクエスト編
    http://genesis-tdsg.blogspot.jp/2013/09/jmockitweb.html

  6. 最強モックツール JMockit その4 内部newクラス
    http://genesis-tdsg.blogspot.jp/2013/09/jmockitnew.html

  7. 最強モックツール JMockit その5 privateメソッド
    http://genesis-tdsg.blogspot.jp/2013/09/jmockitprivate.html

  8. 最強モックツール JMockit その6 staticクラス
    http://genesis-tdsg.blogspot.jp/2013/09/jmockitstatic.html

  9. 最強モックツール JMockit その7 複数回呼び出し
    http://genesis-tdsg.blogspot.jp/2013/09/jmockit.html

  10. 最強モックツール JMockit その8 例外実行
    http://genesis-tdsg.blogspot.jp/2013/09/jmockit_30.html

  11. 最強モックツール JMockit その9 メソッド丸替え
    http://genesis-tdsg.blogspot.jp/2013/10/jmockit.html

  12. 最強モックツール JMockit その10 コンストラクタ差し替え
    http://genesis-tdsg.blogspot.jp/2013/10/jmockit_21.html

  13. 最強モックツール JMockit その11 Eclipseプラグイン
    http://genesis-tdsg.blogspot.jp/2013/10/jmockiteclipse.html

  14. 【追加】最強モックツール JMockit その12 カバレッジオプション
    http://genesis-tdsg.blogspot.jp/2014/04/jmockit.html

  15. 【追加】最強モックツール JMockit その13 DI(Spring)対応
    http://genesis-tdsg.blogspot.jp/2014/05/jmockitdi.html

  16. 次世代のモックフレームワークであるJMockitの基本的な使い方
    http://d.hatena.ne.jp/ryoasai/20110107/1294427245

  17. JMockit使い方メモ
    http://qiita.com/opengl-8080/items/a49d4dae9067413ccdd6

  18. jmockitを使ったシステム時刻のモックとJVMの制限
    http://haws.haw.co.jp/tech/jmockit-date-mock-jvm-limitation/

手順

ブランチの作成

  1. IntelliJ IDEA で 1.0.x-jmockit ブランチを作成します。

build.gradle の修正

JMockit のライブラリをダウンロードして利用可能な状態にします。

  1. build.gradle を リンク先の内容 に変更します。

  2. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  3. Getting started with the JMockit toolkit に「When using JUnit 4.5+, verify that jmockit.jar (or the equivalent Maven dependency) appears before JUnit in the classpath. 」と書かれていますので、classpath 内での jmockit.jar の位置を JUnit より前になるようにします。

  4. メイン画面のメニューから「File」-「Project Structure...」を選択します。

  5. 「Project Structure」ダイアログが表示されます。画面左側のリストから「Project Settings」-「Modules」を選択します。

  6. 画面右側で「Dependencies」タブを選択した後、jmockit のライブラリを junit の上に移動します。移動後「OK」ボタンをクリックしてダイアログを閉じます。

    f:id:ksby:20150606233841p:plain

MailsendService クラスをモックにして MailsendController クラスのテストを実行する

  1. まずはモッククラスを定義しない簡単なテストを作成して MailsendController クラス、MailsendService クラスのメソッドがどちらも呼び出されることを確認します。

  2. src/test/java/ksbysample/webapp/email/web/mailsend の下に MailsendControllerTestByJMockit.java を新規作成します。作成後、リンク先のその1の内容 に変更します。

  3. ソースに breakpoint を設定します。src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendController クラスの send メソッド内の以下の位置に breakpoint を設定します。

    f:id:ksby:20150607084900p:plain

    同じパッケージ内にある MailsendService クラスの saveAndSendEmail メソッド内の以下の位置に breakpoint を設定します。

    f:id:ksby:20150607085128p:plain

  4. テストを Debug 実行します。MailsendControllerTestByJMockit クラス内の「MailsendServiceがモックの場合のテスト」クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Debug」-「MailsendControllerTestByJMockit$MailsendServiceがモックの場合のテスト」メニューを選択します。

    f:id:ksby:20150607085533p:plain

    テストが実行され、設定した2箇所の breakpoint を通過しテストが成功することが確認できます。

  5. 次に MailsendService クラスをモックにします。

  6. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその2の内容 に変更します。

  7. 再度テストを Debug 実行します。MailsendControllerTestByJMockit クラス内の「MailsendServiceがモックの場合のテスト」クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Debug 'MailsendControllerTestByJMocki...'」メニューを選択します。

    テストが実行されますが、今度は MailsendController クラスの breakpoint のみ通過し、MailsendService クラスの breakpoint は通過しませんでした。

  8. ここまでの内容をまとめると以下のようになります。

    • テストクラス内に @Mocked アノテーションを付加したフィールドを定義すれば、Spring が自動生成する対象のクラスでも自動でモックに入れ替わります。
    • 今回のテストの場合、MailsendController クラスの mailsendService フィールドにモックのインスタンスをセットし直す処理は必要ありませんでした。いろいろ見た blog の記事では Deencapsulation.setField(...) でセットし直すと書かれていたのですが、JMockit の機能が強化されたのか不要になったようです ( JMockit のサイトのドキュメントではおそらく Instantiation and injection of tested classes に書かれていると思われるのですがよく分かりませんでした )。
    • void のメソッドの場合 ( MailsendService::saveAndSendEmail ) には、モッククラスのメソッドを定義する必要もありませんでした。

モッククラスは定義したテストクラスだけで有効なのか?

  1. 1つのテストクラス内に2つネストクラスを用意し、片方はモッククラスを定義し、片方はモッククラスを定義しません。

  2. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその3の内容 に変更します。

  3. テストを Debug 実行します。MailsendControllerTestByJMockit クラス内の「MailsendServiceがモックの場合のテスト」クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Debug 'MailsendControllerTestByJMocki...'」メニューを選択します。

    テストは「実体の場合」→「モックの場合」の順で実行されました。

    f:id:ksby:20150607112518p:plain

    breakpoint の通過は以下の状況でした。

    • 最初の「実体の場合」は、設定した2箇所の breakpoint を通過しました。
    • 次の「モックの場合」は、MailsendController クラスの breakpoint のみ通過し、MailsendService クラスの breakpoint は通過しませんでした。
  4. テストが「モックの場合」→「実体の場合」の順で実行されるように変更します。src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその4の内容 に変更します。

  5. 再度テストを Debug 実行します。テストは「モックの場合」→「実体の場合」の順で実行されました。

    f:id:ksby:20150607120541p:plain

    breakpoint の通過は以下の状況でした。

    • 「実体の場合」も「モックの場合」も MailsendController クラスの breakpoint のみ通過し、MailsendService クラスの breakpoint は通過しませんでした。
  6. まとめると以下のようになります。

    • モッククラスが定義されたテストクラスが実行された時に DI されるクラスはモッククラスに入れ替わります。
    • 入れ替わりはモッククラスが定義されたテストクラスのテストメソッド終了後も続きます。元に戻りませんでした。
  7. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその2の内容 に戻します。

  8. 設定した breakpoint を全て解除します。

中で呼び出されるクラスの private メソッドをモックメソッドに入れ替える

  1. MAIL001MailHelper クラスの private メソッド generateTextUsingVelocity だけをモックメソッドに変更してみます。

  2. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその5の内容 に変更します。

  3. テストを実行します。MailsendControllerTestByJMockit クラス内の「MailsendServiceがモックの場合のテスト」クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendControllerTestByJMocki...' with Coverage」メニューを選択します。

    モックメソッドにより送信されるメールの本文が "これはテストです。" に変更され、テストが成功します。

    f:id:ksby:20150607203917p:plain

モックメソッドメソッド内の処理を変更する

  1. 1つ前では MAIL001MailHelper クラスの private メソッド generateTextUsingVelocity のモックメソッドを定義し、result に文字列をセットすることで戻り値を変更しましたが、今度はモックメソッドの処理を実装してみます。

  2. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその6の内容 に変更します。

  3. テストを実行します。モックメソッドにより送信されるメールの本文が "あ,い" に変更され、テストが成功します。

    f:id:ksby:20150607214112p:plain

JMockit でシステム日付を変更して指定した日付のメールを送信する

  1. System.currentTimeMillis() のモックメソッドを作成して日付を変更してメールを送信するテストを作成してみます。

  2. src/test/java/ksbysample/webapp/email/test の下に MockSystem.java を新規作成します。作成後、リンク先の内容 に変更します。

  3. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTestByJMockit.javaリンク先のその7の内容 に変更します。

  4. テストを実行します。MailsendControllerTestByJMockit クラス内の「MailsendServiceがモックの場合のテスト」クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendControllerTestByJMocki...' with Coverage」メニューを選択します。

    テストは成功し、「最小値で送信ボタンをクリックした場合」で変更した日付は「最小値で送信ボタンをクリックした場合2」のテストの時には元に戻っていました。

    f:id:ksby:20150608064619p:plain

反映の取り消し、1.0.x-jmockit ブランチの削除

  1. 今回の変更は反映しません。ブランチを 1.0.x に戻し、git reset --hard コマンドで反映を取り消して 1.0.x-jmockit ブランチを削除します。

感想

  • いろいろな blog に書かれているとおり、最強と呼ばれるのにふさわしく確かに何でもできそうです。
  • 1点だけ気になったのは、どこかでモッククラスを定義したままテストクラスを commit&push してしまうと、テストは通っているのに途中から実はモックでした、という問題が起きそうな気がしました。テストは必ずモックで行うというクラスやメソッドでなければ、モックは手元で開発している時だけ使用しバージョン管理システムには commit&push しない等のルールが必要かもしれません。

次回は。。。

  • Spring Boot の 1.2.4 がリリースされていましたので、バージョンアップします。
  • その後で送信済メール検索機能を実装します。

ソースコード

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41"
    
    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.2.0")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.2")
    compile("com.google.guava:guava:18.0")
    testCompile("org.dbunit:dbunit:2.5.0")
    testCompile("org.subethamail:subethasmtp:3.1.7")
    testCompile("com.icegreen:greenmail:1.4.1")
    testCompile("org.jmockit:jmockit:1.17")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.2.0")
    domaGenRuntime("${jdbcDriver}")
}
  • dependencies の中に testCompile("org.jmockit:jmockit:1.17") を追加します。

MailsendControllerTestByJMockit.java

■その1

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Deencapsulation;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.integration.junit4.JMockit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.MessagingException;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

}

■その2

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Deencapsulation;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.integration.junit4.JMockit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.MessagingException;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Mocked
        private MailsendService mailsendService;
        
        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

}
  • private MailsendService mailsendService; を追加します。

■その3

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Mocked;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Mocked
        private MailsendService mailsendService;
        
        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceが実体の場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

}
  • 「MailsendServiceが実体の場合のテスト」ネストクラスを追加します。こちらにはモッククラスは定義しません。

■その4

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Mocked;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceが実体の場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Mocked
        private MailsendService mailsendService;
        
        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));
        }

    }

}
  • 「MailsendServiceが実体の場合のテスト」ネストクラスを「MailsendServiceがモックの場合のテスト」ネストクラスの上に移動します。

■その5

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.helper.mail.MAIL001MailHelper;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.internet.MimeMessage;

import static mockit.Deencapsulation.invoke;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Mocked("generateTextUsingVelocity")
        private MAIL001MailHelper mockMAILl001MailHelper;

        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            new NonStrictExpectations() {{
                invoke(mockMAILl001MailHelper, "generateTextUsingVelocity", withAny(MailsendForm.class)); result = "これはテストです。";
            }};
            
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));

            assertThat(mailServer.getMessagesCount(), is(1));
            MimeMessage receiveMessage = mailServer.getFirstMessage();
            assertThat(receiveMessage.getContent(), is("これはテストです。"));
        }

    }

}
  • @Mocked private MailsendService mailsendService; を削除します。
  • @Mocked("generateTextUsingVelocity") private MAIL001MailHelper mockMAILl001MailHelper; を追加します。モックにするメソッド名を @Mocked アノテーションに記述することで、そのメソッドだけがモックになり、他は実際のクラスの実装が使用されます。
  • 「最小値で送信ボタンをクリックした場合」テストメソッドの中に new NonStrictExpectations() {{ ... }}; を追加し、mockit.Deencapsulation.invoke メソッドで generateTextUsingVelocity の戻り値を定義します。
  • invoke メソッドにモックメソッドの引数を指定するのですが、Object 型の場合には any ではなく withAny(...) で指定しなければならない、という点が非常に分かりずらかったです。anyString 等の例はよく見かけれるのですが、withAny の例を見つけるまで結構時間がかかりました。

■その6

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.helper.mail.MAIL001MailHelper;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.MockMvcResource;
import ksbysample.webapp.email.test.TestDataResource;
import ksbysample.webapp.email.test.TestHelper;
import mockit.Delegate;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.internet.MimeMessage;

import static mockit.Deencapsulation.invoke;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;

        @Mocked("generateTextUsingVelocity")
        private MAIL001MailHelper mockMAILl001MailHelper;

        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            new NonStrictExpectations() {{
                invoke(mockMAILl001MailHelper, "generateTextUsingVelocity", withAny(MailsendForm.class));
                result = new Delegate<String>() {
                    public String delegate(MailsendForm mailsendForm) {
                        return mailsendForm.getName() + "," + mailsendForm.getNaiyo();
                    }
                };
            }};
            
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));

            assertThat(mailServer.getMessagesCount(), is(1));
            MimeMessage receiveMessage = mailServer.getFirstMessage();
            assertThat(receiveMessage.getContent(), is("あ,い"));
        }

    }

}
  • 最小値で送信ボタンをクリックした場合テストメソッドnew NonStrictExpectations() {{ ... }}; の中で、result = new Delegate<String>() { ... }; のように実装を変更します。Delegate の後の <...> の中に指定するのは戻り値の型です。
  • テストメソッドの最後の assertThat を assertThat(receiveMessage.getContent(), is("あ,い")); に変更します。

■その7

package ksbysample.webapp.email.web.mailsend;

import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.*;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.internet.MimeMessage;
import java.util.Calendar;
import java.util.Date;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class MailsendControllerTestByJMockit {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public static class MailsendServiceがモックの場合のテスト {

        private final MailsendForm mailsendFormMin
                = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public MailServerResource mailServer;

        @Rule
        @Autowired
        public MockMvcResource mvc;
        
        @Test
        public void 最小値で送信ボタンをクリックした場合() throws Exception {
            // 日付を 2015/01/01 にする
            MockSystem.setCurrentDate(2015, Calendar.JANUARY, 1, 0, 0, 0);

            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMin))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("/mailsend"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(model().errorCount(0));

            assertThat(mailServer.getMessagesCount(), is(1));
            MimeMessage receiveMessage = mailServer.getFirstMessage();
            Calendar calendar = Calendar.getInstance();
            calendar.set(2015, Calendar.JANUARY, 1, 0, 0, 0);
            System.out.println("★★★ " + receiveMessage.getSentDate());
            assertThat(receiveMessage.getSentDate(), is(calendar.getTime()));
        }

        @Test
        public void 最小値で送信ボタンをクリックした場合2() {
            Date now = new Date();
            System.out.println("■■■ " + now);
        }
        
    }

}
  • テストメソッドの実行順序をテストメソッド名順にしたいので、「MailsendServiceがモックの場合のテスト」テストクラスに @FixMethodOrder(MethodSorters.NAME_ASCENDING) を付加します。
  • 「最小値で送信ボタンをクリックした場合」テストメソッドの処理の最初に MockSystem.setCurrentDate(2015, Calendar.JANUARY, 1, 0, 0, 0); を追加します。
  • 「最小値で送信ボタンをクリックした場合」テストメソッド内のメールのチェック内容を、receiveMessage.getSentDate() をチェックするものに変更します。
  • 「最小値で送信ボタンをクリックした場合2」テストメソッドを追加します。「最小値で送信ボタンをクリックした場合」テストメソッドで変更した日付が、他のテストメソッドでも変更されたままなのかを確認するためのテストメソッドです。

MockSystem.java

package ksbysample.webapp.email.test;

import mockit.Mock;
import mockit.MockUp;

import java.util.Calendar;

public class MockSystem extends MockUp<System> {

    private static final MockSystem instance = new MockSystem();
    
    private long mockDate;

    private MockSystem() {
    }
    
    public static void setCurrentDate(int year, int month, int date, int hourOfDay, int minute, int second) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(year, month, date, hourOfDay, minute, second);
        instance.mockDate = calendar.getTime().getTime();
    }

    @Mock
    public long currentTimeMillis() {
        return this.mockDate;
    }

}

履歴

2015/06/08
初版発行。