Spring Boot でメール送信する Web アプリケーションを作る ( その12 )( メール送信画面の作成6 )
概要
Spring Boot でメール送信する Web アプリケーションを作る ( その11 )( メール送信画面の作成5 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- メール送信画面の作成
- 前回から引き続きテストクラスを書きます。ただし今回は web/mailsend/MailsendService のテストクラスのみです ( いろいろ試していたら時間がかかりりました )。web/mailsend/MailsendController のテストクラスは次回にします。
ソフトウェア一覧
参考にしたサイト
Java言語で固定要素のListを初期化する際のイディオム
http://d.hatena.ne.jp/ryoasai/20101226/1293350856- TestDataResource クラスで List
の定数を初期化する処理を実装する時に参考にしました。
- TestDataResource クラスで List
What is simplest way to read a file into String?
http://stackoverflow.com/questions/3402735/what-is-simplest-way-to-read-a-file-into-string- ファイルの中身を String 型の変数に一気に読み込む方法を調査した時に参考にしました。
guava-libraries
https://code.google.com/p/guava-libraries/Java7 では Charset の指定がプチ改善されてます
http://etc9.hatenablog.com/entry/20120218/1329587059- Charset クラスの文字コードの指定方法を調査した時に参考にしました。
Java resource as file
http://stackoverflow.com/questions/676097/java-resource-as-file- resources ディレクトリの下にあるファイルから File クラスのインスタンスを生成する方法を調査した時に参考にしました。
データベースのテスト支援ツール DbUnit その3 DB参照編
http://genesis-tdsg.blogspot.jp/2013/08/dbunit-db.html- DbUnit を使用してテーブルのデータをチェックする方法を調査した時に参考にしました。
IntelliJ IDEAで空行インデントを保持する
http://takashabe.hatenablog.com/entry/2013/05/17/125150- IntelliJ IDEA の空白を取り除く設定を解除する方法を調査した時に参照しました。
手順
web/mailsend/MailsendService のテストクラスの作成
ksbysample-webapp-basic から以下のファイルをコピーします。
※src/test/java/ksbysample/webapp/basic の下のファイルは src/test/java/ksbysample/webapp/email の下へコピーします。また package も ksbysample.webapp.email へ変更します。
src/test/java/ksbysample/webapp/email/test の下の TestDataResource.java を リンク先の内容 に変更します。
テスト時の初期データを作成します。src/test/resources の下に testdata ディレクトリを作成します。
src/test/resources/testdata の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。
- テスト開始時はテーブルの中に何もデータが入っていないようにしますので、CSV ファイルにはデータは何も記述しません。
テスト実行後の CSV ファイルとテーブルのデータを比較するクラスを作成します。src/test/java/ksbysample/webapp/email/test の下に TableDataAssert.java を新規作成します。作成後、リンク先の内容 に変更します。
mailsendForm_simple.yml でテストした時にテーブルデータと比較する CSV ファイルを作成します。src/test/java/ksbysample/webapp/email/web/mailsend/testdata の下に simple ディレクトリ、minimum ディレクトリを作成します。
src/test/java/ksbysample/webapp/email/web/mailsend/testdata/simple の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/email/web/mailsend/testdata/minimum の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。
生成されるメール本文をテキストファイルに記述しておいて、ファイルから String 型の変数に読み込んだ後 assertThat でチェックします。ファイルから String 型の変数に読み込む処理で Guava の com.google.common.io.Files の toString メソッドを使用します。
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。
テストで生成されるメール本文を記述したテキストファイルを作成します ( テストで assertThat で比較するために使用します ) 。src/test/resources/ksbysample/webapp/email/web/mailsend の下に mailsendForm_simple_mail.txt, mailsendForm_minimum_mail.txt を新規作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/email/web/mailsend の MailsendService.java から MailsendServiceTest.java を生成します。
src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendServiceTest.java を リンク先の内容 に変更します。
テストを実行します。MailsendServiceTest.java のクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendServiceTest' with Coverage」メニューを選択します。
テストが実行されますが、「MailsendFormの必須項目のみ値がセットされている場合」のテストが失敗しました。
原因は MailsendService クラスの saveEmail メソッドで getItem() で取得したリストの Null チェックをせずに処理していたため、NullPointerException が発生していたためでした。
src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendService.java を リンク先の内容 に変更します。
再度テストを実行しますが、また「MailsendFormの必須項目のみ値がセットされている場合」のテストが失敗しました。
今回は以下の原因でした。
- MAIL001MailHelper クラスの generateTextUsingVelocity メソッドで mailsendForm.getItem() で取得した値が null の場合に
model.put("item", ...);
を呼び出して "item" のデータをセットしていないため、Velocity のテンプレートの$item
が置換されていませんでした。 - MAIL001MailHelper クラスの generateTextUsingVelocity メソッドで Velocity のテンプレートに渡す変数 model に null のデータがセットされると、テンプレートの変数が置換されないためでした。Velocity では値が null のデータは置換しないようです ( テンプレートファイルの $xxx の記述がそのまま残ります )。
- src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_minimum_mail.txt を作成した時に ":" の後に空白を1つ入れていたのですが、IntelliJ IDEA は行末の空白を自動で取り除くため Velocity で生成したメール本文と差異が発生していました。
- 同じく src/main/resources/templates/mail/MAIL001 の下の MAIL001-body.vm の "内容 :" の後に入れていた空白も削除されていました。
- MAIL001MailHelper クラスの generateTextUsingVelocity メソッドで mailsendForm.getItem() で取得した値が null の場合に
src/main/java/ksbysample/webapp/email/helper/mail の下の MAIL001MailHelper.java を リンク先の内容 に変更します。
Velocity に渡す変数 model にセットされた値が null の場合には空文字列に置換する処理を追加します。src/main/java/ksbysample/webapp/email/util の下の VelocityUtils.java を リンク先の内容 に変更します。
IntelliJ IDEA が行末の空白を自動で取り除かないよう設定を変更します。メインメニューから「File」-「Settings...」を選択します。
「Settings」ダイアログが表示されます。画面左側のツリーから「Editor」-「General」を選択後、画面右側の下部にある「Strip trailing spaces on Save」で「None」を選択し「OK」ボタンをクリックします。
※あとで Spring Boot でメール送信する Web アプリケーションを作る ( その3 )( Project の作成 ) にこの手順を追加しておきます。
src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_minimum_mail.txt で、各行の ":" の後に空白を1つ追加します。また "内容 : " の次に空行を1行追加します。
src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_simple_mail.txt も "内容 :" の後に空白を1つ追加します。
src/main/resources/templates/mail/MAIL001 の下の MAIL001-body.vm も "内容 :" の後に空白を1つ追加します。
再度テストを実行します。今度はテストが正常に終了して「All Test Passed」の文字が表示されました。
次回は。。。
web/mailsend/MailsendController のテストクラスを書きます。commit 等も次回です。
ソースコード
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") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.2.0") domaGenRuntime("${jdbcDriver}") }
compile("com.google.guava:guava:18.0")
を追加します。
TestDataResource.java
package ksbysample.webapp.email.test; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.database.QueryDataSet; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.ReplacementDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.dbunit.dataset.xml.FlatXmlDataSet; import org.dbunit.dataset.xml.FlatXmlDataSetBuilder; import org.dbunit.operation.DatabaseOperation; import org.junit.rules.ExternalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.io.File; import java.io.FileOutputStream; import java.nio.file.Files; import java.util.Arrays; import java.util.List; @Component public class TestDataResource extends ExternalResource { private final List<String> BACKUP_TABLES = Arrays.asList( "email" , "email_item" ); private final String BACKUP_FILE_NAME = "ksbyemail_backup"; @Autowired private DataSource dataSource; private File backupFile; @Override protected void before() throws Exception { IDatabaseConnection conn = null; try { conn = new DatabaseConnection(dataSource.getConnection()); // バックアップを取得する QueryDataSet partialDataSet = new QueryDataSet(conn); for (String backupTable: BACKUP_TABLES) { partialDataSet.addTable(backupTable); } ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet); replacementDatasetBackup.addReplacementObject("", "[null]"); backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml"); try (FileOutputStream fos = new FileOutputStream(backupFile)) { FlatXmlDataSet.write(replacementDatasetBackup, fos); } // テストデータに入れ替える IDataSet dataSet = new CsvDataSet(new File("src/test/resources/testdata")); ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet); replacementDataset.addReplacementObject("[null]", null); DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset); } finally { if (conn != null) conn.close(); } } @Override protected void after() { try { IDatabaseConnection conn = null; try { conn = new DatabaseConnection(dataSource.getConnection()); // バックアップからリストアする if (backupFile != null) { IDataSet dataSet = new FlatXmlDataSetBuilder().build(backupFile); ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet); replacementDatasetRestore.addReplacementObject("[null]", null); DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore); } } finally { if (backupFile != null) { Files.delete(backupFile.toPath()); backupFile = null; } try { if (conn != null) conn.close(); } catch (Exception ignored) {} } } catch (Exception e) { e.printStackTrace(); } } }
- before メソッドの以下の点を変更します。
partialDataSet.addTable(...)
でバックアップするテーブルを email, email_item に変更します。private final List<String> BACKUP_TABLES
を追加し、before メソッド内で for 文で処理するようにします。- for 文ではなく Stream API で
BACKUP_TABLES.stream().forEach(backupTable -> partialDataSet.addTable(backupTable));
と処理するよう実装したらなぜか AmbiguousTableNameException を catch しないとダメというエラーが出ました。catch 文をわざわざ追加したくなかったので for 文で処理するようにしたのですが、なぜ Stream API を使用するとこうなるのか原因が分かりませんでした。。。 File.createTempFile(...)
で指定するバックアップファイル名を "ksbyemail_backup" へ変更します。private final String BACKUP_FILE_NAME = "ksbyemail_backup";
を追加し、File.createTempFile(...)
の引数には BACKUP_FILE_NAME を渡します。
testdata/email.csv, email_item.csv, table-ordering.txt
■email.csv
"email_id","from_addr","to_addr","subject","name","sex","type","naiyo"
■email_item.csv
"email_item_id","email_id","item"
■table-ordering.txt
email email_item
TableDataAssert.java
package ksbysample.webapp.email.test; import org.dbunit.Assertion; import org.dbunit.DatabaseUnitException; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.dataset.DataSetException; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.ITable; import org.dbunit.dataset.ReplacementDataSet; import org.dbunit.dataset.filter.DefaultColumnFilter; import javax.sql.DataSource; import java.sql.SQLException; public class TableDataAssert { private final IDataSet dataSet; private final DataSource dataSource; public TableDataAssert(IDataSet dataSet, DataSource dataSource) { ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet); replacementDataset.addReplacementObject("[null]", null); this.dataSet = replacementDataset; this.dataSource = dataSource; } public void assertEquals(String tableName, String[] columnNames) throws DatabaseUnitException, SQLException { ITable expected = expectedTable(tableName, columnNames); ITable actual = actualTable(tableName, columnNames); Assertion.assertEquals(expected, actual); } private ITable expectedTable(String tableName, String[] columnNames) throws DataSetException { ITable table = dataSet.getTable(tableName); if (columnNames != null) { table = DefaultColumnFilter.excludedColumnsTable(table, columnNames); } return table; } private ITable actualTable(String tableName, String[] columnNames) throws DatabaseUnitException, SQLException { IDatabaseConnection conn = new DatabaseConnection(this.dataSource.getConnection()); ITable table = conn.createDataSet().getTable(tableName); if (columnNames != null) { table = DefaultColumnFilter.excludedColumnsTable(table, columnNames); } return table; } }
mailsend/test/data/simple/email.csv, email_item.csv, table-ordering.txt
■email.csv
"email_id","from_addr","to_addr","subject","name","sex","type","naiyo" "","test@sample.com","xxx@yyy.zzz","テスト","田中 太郎","1","1","これはテストです。"
■email_item.csv
"email_item_id","email_id","item" "","","101" "","","102" "","","103"
■table-ordering.txt
email email_item
mailsend/test/data/minimum/email.csv, email_item.csv, table-ordering.txt
■email.csv
"email_id","from_addr","to_addr","subject","name","sex","type","naiyo" "","test@sample.com","xxx@yyy.zzz","テスト","[null]","[null]","[null]","[null]"
- null の場合には
""
ではなく"[null]"
と書くこと。[null]→null への変換は ksbysample.webapp.email.test.TableDataAssert クラスのコンストラクタで行っています。
■email_item.csv
"email_item_id","email_id","item"
■table-ordering.txt
email email_item
mailsendForm_simple_mail.txt, mailsendForm_minimum_mail.txt
■mailsendForm_simple_mail.txt
氏名 : 田中 太郎 性別 : 男性 項目 : 資料請求 商品 : 商品1, 商品2, 商品3 内容 : これはテストです。
■mailsendForm_minimum_mail.txt
氏名 : 性別 : 項目 : 商品 : 内容 :
MailsendServiceTest.java
package ksbysample.webapp.email.web.mailsend; import com.google.common.io.Files; import ksbysample.webapp.email.Application; import ksbysample.webapp.email.test.MailServerResource; import ksbysample.webapp.email.test.TableDataAssert; import ksbysample.webapp.email.test.TestDataResource; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.junit.Rule; import org.junit.Test; 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.InternetAddress; import javax.mail.internet.MimeMessage; import javax.sql.DataSource; import java.io.File; import java.nio.charset.StandardCharsets; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class MailsendServiceTest { private final MailsendForm mailsendFormSimple = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_simple.yml")); private final MailsendForm mailsendFormMinimum = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_minimum.yml")); @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public MailServerResource mailServer; @Autowired private DataSource dataSource; @Autowired private MailsendService mailsendService; @Test public void MailsendFormの全てに値がセットされている場合() throws Exception { mailsendService.saveAndSendEmail(mailsendFormSimple); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/simple")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); String mailsendFormSimpleMail = Files.toString(new File(getClass().getResource("mailsendForm_simple_mail.txt").toURI()), StandardCharsets.UTF_8); assertThat(receiveMessage.getContent(), is(mailsendFormSimpleMail)); } @Test public void MailsendFormの必須項目のみ値がセットされている場合() throws Exception { mailsendService.saveAndSendEmail(mailsendFormMinimum); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/minimum")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); String mailsendFormMinimumMail = Files.toString(new File(getClass().getResource("mailsendForm_minimum_mail.txt").toURI()), StandardCharsets.UTF_8); assertThat(receiveMessage.getContent(), is(mailsendFormMinimumMail)); } }
MailsendService.java
public void saveEmail(MailsendForm mailsendForm) { // email テーブルに保存する Email email = new Email(); BeanUtils.copyProperties(mailsendForm, email); emailDao.insert(email); // email_item テーブルに保存する EmailItem emailItem = new EmailItem(); if (mailsendForm.getItem() != null) { for (String item : mailsendForm.getItem()) { emailItem.setEmailItemId(null); emailItem.setEmailId(email.getEmailId()); emailItem.setItem(item); emailItemDao.insert(emailItem); } } }
- null チェックの if 文
if (mailsendForm.getItem() != null) { ... }
を追加します。
MAIL001MailHelper.java
private String generateTextUsingVelocity(MailsendForm mailsendForm) { Constant constant = Constant.getInstance(); Map<String, Object> model = new HashMap<>(); model.put("name", mailsendForm.getName()); model.put("sex", constant.SEX_MAP.get(mailsendForm.getSex())); model.put("type", constant.TYPE_MAP.get(mailsendForm.getType())); String itemList = null; if (mailsendForm.getItem() != null) { itemList = mailsendForm.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", ")); } model.put("item", itemList); model.put("naiyo", mailsendForm.getNaiyo()); return velocityUtils.merge(this.templateLocation, model); }
model.put("item", itemList);
が必ず呼び出されるようif (mailsendForm.getItem() != null) { ... }
の外に出します。- ただし mailsendForm.getItem() の結果が null の場合には null がセットされるようにしたいので、if 文の外に
String itemList = null;
を追加します。
VelocityUtils.java
package ksbysample.webapp.email.util; import org.apache.velocity.app.VelocityEngine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.ui.velocity.VelocityEngineUtils; import java.util.HashMap; import java.util.Map; @Component public class VelocityUtils { @Autowired private VelocityEngine velocityEngine; @Value("${spring.velocity.charset}") private String charset; public String merge(String templateLocation, Map<String, Object> model) { // model 内の値が null のデータは空文字列に入れ替える Map<String, Object> modelForVelocity = new HashMap<>(); for (Map.Entry<String, Object> e : model.entrySet()) { modelForVelocity.put(e.getKey(), (e.getValue() == null ? "" : e.getValue())); } return VelocityEngineUtils.mergeTemplateIntoString(this.velocityEngine, templateLocation, charset, modelForVelocity); } }
- model 内の値が null の場合に空文字列に入れ替える処理を追加します。model のデータは変更したくなかったので、置換結果は別の変数に保存するようにしました。
VelocityEngineUtils.mergeTemplateIntoString(...)
の第4引数に渡す変数を model → modelForVelocity に変更します。
履歴
2015/05/20
初版発行。