Spring Boot でメール送信する Web アプリケーションを作る ( その13 )( メール送信画面の作成7 )
概要
Spring Boot でメール送信する Web アプリケーションを作る ( その12 )( メール送信画面の作成6 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- メール送信画面の作成
- 前回から引き続きテストクラスを書きます。web/mailsend/MailsendController のテストクラスです。
ソフトウェア一覧
参考にしたサイト
手順
Controller クラスのテストを作成する前にちょっと悩みました。。。
悩んだのは、Service クラスのテストでデータの保存やメール送信のテストを実施しましたが、Controller クラスのテストでもデータの保存、メール送信のテストまで実施した方がよいのか? という点です。
Controller クラスのテストでは最低でも入力チェックと画面遷移のテストはやるべきものだと思いますので、さすがにそれに加えてデータ保存やメール送信までテストするのはどう考えてもやりすぎですよね。。。 クラスをきちんと分けておけば、余程の理由がない限り Service クラスで実施したテストを Controller クラスのテストで重複して実施する必要性も感じません。
今回あまり深く考えずに Controller クラスと Service クラスに分けていたのですが、
- テストの観点から考えると、Controller クラスと Service クラスはきちんと分けておいた方がテストし易いように思えました。
- Controller クラスのテストは入力チェックと画面遷移をテストします。
- Service クラスは入力チェックを通過した後のメインの処理 ( DB へのデータ保存等 ) をテストします。
という方針でいこうと思います。
web/mailsend/MailsendController のテストクラスの雛形の作成
以下の順序でテストクラスを作成します。
src/main/java/ksbysample/webapp/email/web/mailsend の MailsendController.java から MailsendControllerTest.java を生成します。
構造化しながらテストメソッドの定義だけを記述します。src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTest.java を リンク先のその1の内容 に変更します。
MockMvcResource クラスの作成
ksbysample-webapp-basic から以下のファイルをコピーしてファイル名を変更します。
src/test/java/ksbysample/webapp/email/test の下の MockMvcResource.java を リンク先の内容 に変更します。
初期表示のテストの作成
src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTest.java を リンク先のその2の内容 に変更します。
テストを実行します。ネストしたクラスのクラス名「初期表示のテスト」にカーソルを移動した後、コンテキストメニューを表示して「Run '初期表示のテスト' with Coverage」メニューを選択します。
テストが全て成功することを確認します。
入力チェックエラーのテストの作成
ksbysample-webapp-basic から以下のファイルをコピーします。
テストで使用する MailsendForm クラスのテストデータを作成します。src/test/resources/ksbysample/webapp/email/web/mailsend の下に mailsendForm_empty.yml, mailsendForm_max_patternerr.yml, mailsendForm_fv01.yml, mailsendForm_fv02.yml を新規作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTest.java を リンク先のその3の内容 に変更します。
テストを実行します。ネストしたクラスのクラス名「入力チェックエラーのテスト」にカーソルを移動した後、コンテキストメニューを表示して「Run '入力チェックエラーのテスト' with Coverage」メニューを選択します。
「項目が資料請求_商品に関する苦情の場合に商品が何も選択されていない場合には入力チェックエラー」のテストが失敗しました。
原因は MailsendFormValidator クラスの validate メソッドの
if (mailsendForm.getItem() == null) {
のところで mailsendForm.getItem() が null ではなく mailsendForm.getItem().size() == 0 の状態になっており、入力チェックエラーにならないためでした。src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendFormValidator.java を リンク先の内容 に変更します。
再度テストを実行します。今度はテストが全て成功します。
正常処理時のテストの作成
テストで使用する MailsendForm クラスのテストデータを作成します。src/test/resources/ksbysample/webapp/email/web/mailsend の下に mailsendForm_min.yml, mailsendForm_max.yml を新規作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendControllerTest.java を リンク先のその4の内容 に変更します。
テストを実行します。ネストしたクラスのクラス名「正常処理時のテスト」にカーソルを移動した後、コンテキストメニューを表示して「Run '正常処理時のテスト' with Coverage」メニューを選択します。
「最小値で送信ボタンをクリックした場合」と「最大値で送信ボタンをクリックした場合」のテストが失敗しました。
原因は TestHelper クラスの postForm メソッドのバグでした。MailsendController クラスの send メソッドが呼び出された時点で mailsendForm.getItem() で取得したリストの値の前後に "[...]" という余計な括弧が付いていました。付かないように修正します。
src/test/java/ksbysample/webapp/email/test の下の TestHelper.java を リンク先の内容 に変更します。
再度テストを実行します。今度はテストが全て成功します。
全てのテストメソッドの実行、確認
MailsendControllerTest の全てのテストを実行してみます。テストクラスのクラス名「MailsendControllerTest」にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendControllerTest...' with Coverage」メニューを選択します。
テストが全て成功することが確認できます。
これまで作成した全てのテストを実行してみます。Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」メニューを選択します。
こちらもテストが全て成功することが確認できます。
commit、Push、Pull Request、マージ
commit します。commit 時に Code Analysis のダイアログが表示されますが、TestHelper クラスの Javadoc の Warning と、build.gradle の Grgit 関連の Warning なので、今回は何も対応はせずに「Commit」ボタンをクリックします。
GitHub へ Push、1.0.x-maketest-mailsend -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-maketest-mailsend ブランチを削除、をします。
最後に
最初の「Controller クラスのテストを作成する前にちょっと悩みました。。。」で Controller クラスのテストはどこまでやればよいのか? について考えたことを書きましたが、興味があってモックツール JMockit の調査をしていた時に下記のブログの記事で、
株式会社ジェニシス 技術開発事業部ブログ: 最強モックツール JMockit その4 内部newクラス
しかし、「二つのクラスが合体させてのテスト」というのは、厳密には「結合テスト」と言ってしまって良いでしょう。 単体テストフェーズであるユニットテストで行うのは、もちろん「単体テスト」です。
という記述を見かけました。この記述は自分にはすごく分かりやすくて、1つのテストで複数のクラスの機能をテストしないようにした方がよいのだなと理解できた次第です。どういうテストなのかということを考えることを忘れていましたね。。。
次回は。。。
まだ送信済メール検索機能には進みません。他のメール関連機能を実装したり、モックツール JMockit を試してみたりしたいので、以下の実装・調査を行います。
ソースコード
MailsendControllerTest.java
■その1
package ksbysample.webapp.email.web.mailsend; import ksbysample.webapp.email.Application; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @RunWith(Enclosed.class) public class MailsendControllerTest { @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 初期表示のテスト { @Test public void メール送信画面を表示する() throws Exception { } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 入力チェックエラーのテスト { @Test public void データ未入力時には入力チェックエラー() throws Exception { // NotBlank のテスト } @Test public void 最大文字数オーバー_パターンエラー時には入力チェックエラー() throws Exception { // Email/Size/Pattern のテスト } @Test public void 項目が資料請求_商品に関する苦情の場合に商品が何も選択されていない場合には入力チェックエラー() throws Exception { } @Test public void 項目がその他の場合に内容に何も入力されていない場合には入力チェックエラー() throws Exception { } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 正常処理時のテスト { @Test public void 最小値空ありで送信ボタンをクリックした場合() throws Exception { } @Test public void 最小値で送信ボタンをクリックした場合() throws Exception { } @Test public void 最大値で送信ボタンをクリックした場合() throws Exception { } } }
■その2
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 初期表示のテスト { @Rule @Autowired public MockMvcResource mvc; @Test public void メール送信画面を表示する() throws Exception { // メール送信画面が表示されることを確認する mvc.nonauth.perform(get("/mailsend")) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("mailsend/mailsend")); } }
■その3
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 入力チェックエラーのテスト { private final MailsendForm mailsendFormEmpty = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_empty.yml")); private final MailsendForm mailsendFormMaxPatternerr = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_max_patternerr.yml")); private final MailsendForm mailsendFormFv01 = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_fv01.yml")); private final MailsendForm mailsendFormFv02 = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_fv02.yml")); @Rule @Autowired public MockMvcResource mvc; @Test public void データ未入力時には入力チェックエラー() throws Exception { // NotBlank のテスト mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormEmpty)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("mailsend/mailsend")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(3)) .andExpect(errors().hasFieldError("mailsendForm", "fromAddr", "NotBlank")) .andExpect(errors().hasFieldError("mailsendForm", "toAddr", "NotBlank")) .andExpect(errors().hasFieldError("mailsendForm", "subject", "NotBlank")); } @Test public void 最大文字数オーバー_パターンエラー時には入力チェックエラー() throws Exception { // Email/Size/Pattern のテスト mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMaxPatternerr)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("mailsend/mailsend")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(7)) .andExpect(errors().hasFieldError("mailsendForm", "fromAddr", "Email")) .andExpect(errors().hasFieldError("mailsendForm", "toAddr", "Email")) .andExpect(errors().hasFieldError("mailsendForm", "subject", "Size")) .andExpect(errors().hasFieldError("mailsendForm", "name", "Size")) .andExpect(errors().hasFieldError("mailsendForm", "sex", "Pattern")) .andExpect(errors().hasFieldError("mailsendForm", "type", "Pattern")) .andExpect(errors().hasFieldError("mailsendForm", "naiyo", "Size")); } @Test public void 項目が資料請求_商品に関する苦情の場合に商品が何も選択されていない場合には入力チェックエラー() throws Exception { mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormFv01)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("mailsend/mailsend")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(1)) .andExpect(errors().hasGlobalError("mailsendForm", "mailsendForm.item.noSelect")); } @Test public void 項目がその他の場合に内容に何も入力されていない場合には入力チェックエラー() throws Exception { mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormFv02)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("mailsend/mailsend")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(1)) .andExpect(errors().hasGlobalError("mailsendForm", "mailsendForm.naiyo.noText")); } }
■その4
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 正常処理時のテスト { private final MailsendForm mailsendFormMinimum = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_minimum.yml")); private final MailsendForm mailsendFormMin = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_min.yml")); private final MailsendForm mailsendFormMax = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_max.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.mailsendFormMinimum)) .andExpect(status().isFound()) .andExpect(redirectedUrl("/mailsend")) .andExpect(model().hasNoErrors()) .andExpect(model().errorCount(0)); } @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)); } @Test public void 最大値で送信ボタンをクリックした場合() throws Exception { mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMax)) .andExpect(status().isFound()) .andExpect(redirectedUrl("/mailsend")) .andExpect(model().hasNoErrors()) .andExpect(model().errorCount(0)); } }
MockMvcResource.java
package ksbysample.webapp.email.test; import org.junit.rules.ExternalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @Component public class MockMvcResource extends ExternalResource { @Autowired private WebApplicationContext context; public MockMvc nonauth; @Override protected void before() throws Throwable { this.nonauth = MockMvcBuilders.webAppContextSetup(this.context) .build(); } }
- 今回は Spring Security を使用していないので、フィールドを nonauth のみにし、Spring Security を利用していた部分を削除します。
mailsendForm_empty.yml, mailsendForm_max_patternerr.yml, mailsendForm_fv01.yml, mailsendForm_fv02.yml
■mailsendForm_empty.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: toAddr: subject: name: sex: type: item: naiyo:
■mailsendForm_max_patternerr.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: a@ toAddr: b subject: 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 name: 123456789012345678901234567890123 sex: 3 type: 4 item: naiyo: 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
■mailsendForm_fv01.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: [email protected] toAddr: [email protected] subject: テスト name: 田中 太郎 sex: 1 type: 1 item: naiyo: これはテストです。
■mailsendForm_fv02.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: [email protected] toAddr: [email protected] subject: テスト name: 田中 太郎 sex: 1 type: 3 item: - 101 - 102 - 103 naiyo:
MailsendFormValidator.java
@Override public void validate(Object target, Errors errors) { MailsendForm mailsendForm = (MailsendForm)target; Constant constant = Constant.getInstance(); // 「項目」が「資料請求」「商品に関する苦情」の場合には「商品」が何も選択されていない場合にはエラー if (StringUtils.equals(mailsendForm.getType(), "1") || StringUtils.equals(mailsendForm.getType(), "2")) { if ((mailsendForm.getItem() == null) || (mailsendForm.getItem().size() == 0)) { errors.reject("mailsendForm.item.noSelect"); } } // 「項目」が「その他」の場合には「内容」に何も入力されていない場合にはエラー if (StringUtils.equals(mailsendForm.getType(), "3")) { if (mailsendForm.getNaiyo().length() == 0) { errors.reject("mailsendForm.naiyo.noText"); } } }
if (mailsendForm.getItem() == null) {
→if ((mailsendForm.getItem() == null) || (mailsendForm.getItem().size() == 0)) {
へ変更します。
mailsendForm_min.yml, mailsendForm_max.yml
■mailsendForm_min.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: a@a toAddr: b@b subject: テ name: あ sex: 1 type: 1 item: - 101 naiyo: い
■mailsendForm_max.yml
!!ksbysample.webapp.email.web.mailsend.MailsendForm fromAddr: abcdeabcdeabcdeabcdeabcde@abcdeabcdeabcde.abcdeabcdeabcdeabcde.jp toAddr: abcdeabcdeabcdeabcdeabcde@abcdeabcdeabcde.abcdeabcdeabcdeabcde.jp subject: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 name: 12345678901234567890123456789012 sex: 1 type: 2 item: - 101 - 102 - 103 naiyo: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
TestHelper.java
public static MockHttpServletRequestBuilder postForm(String urlTemplate, Object form) throws IllegalAccessException { MockHttpServletRequestBuilder request = post(urlTemplate).contentType(MediaType.APPLICATION_FORM_URLENCODED); for (Field field : form.getClass().getDeclaredFields()) { field.setAccessible(true); if (field.get(form) == null) { request = request.param(field.getName(), ""); } else if (field.get(form) instanceof List<?>) { for (Object str : (List<?>)field.get(form)) { request = request.param(field.getName(), str.toString()); } } else { request = request.param(field.getName(), field.get(form).toString()); } } return request; }
- postForm メソッドの処理に
else if (field.get(form) instanceof List<?>) { ... }
の部分を追加します。
履歴
2015/05/24
初版発行。