Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その15 )( ログイン画面の作成6 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その14 )( Spring Session を使用する3 ) の続きです。
今回の手順で確認できるのは以下の内容です。
- ログイン画面・ログアウト機能のテストクラスの作成 ( 2回に分けます )
JavaのテストにはAssertJがオススメ の記事を読んで AssertJ に興味が湧いたので、今回からテスト結果の検証には JUnit の AssertThat ではなく AssertJ を使用してみます。
参照したサイト・書籍
JavaのテストにはAssertJがオススメ
http://qiita.com/ikemo/items/165f01740995245f9009assertj examples tests
https://github.com/joel-costigliola/assertj-examples/tree/master/assertions-examples/src/test/java/org/assertj/examples
目次
- 1.0.x-make-test-login ブランチの作成
- AssertJ を使用できるようにする
- テストデータ配置用ディレクトリの作成
- テストデータの作成
- TestDataResource クラスの準備
- UserInfoServiceTest クラスの作成
- LendingUserDetailsTest クラスの作成
- LendingUserDetailsServiceTest クラスの作成
- メモ書き
手順
1.0.x-make-test-login ブランチの作成
- IntelliJ IDEA で 1.0.x-make-test-login ブランチを作成します。
AssertJ を使用できるようにする
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。
テストデータ配置用ディレクトリの作成
- src/test/resources の下に testdata/base ディレクトリを作成します。
テストデータの作成
user_info, user_role テーブルに以下の画像のデータを登録します。
テストデータは以下の内容です。
user_id テストデータの内容 1 ログイン可能、role が全てある 2 ログイン可能、role は ROLE_USER のみ 3 ログイン不可能、enabled = 0 4 ログイン不可能、アカウントの有効期限(expired_account)切れ 5 ログイン不可能、パスワードの有効期限(expired_password)切れ 6 ログイン不可能、role が何も登録されていない 6 ログイン不可能、全部NG ( enabled = 0、アカウント・パスワード有効期限切れ、IDロック中、roleが何も登録されていない ) user_info.password には http://localhost:8080/encode?password=[名前(例:taro)] で出力された文字列をセットしています。
CSVファイル出力時にカラム名も出力されるようにします。Database View の user_info, user_role テーブルでコンテキストメニューを表示し、「Save To File」->「Configure Extractors...」を選択します。
「Data Extractors」ダイアログが表示されますので、画面左側のリストの「Comma-separated Values (CSV)」が選択されていることを確認した後、画面右側の「Include column names」をチェックして「OK」ボタンをクリックします。
CSVファイルを出力します。Database View の user_info, user_role テーブルでコンテキストメニューを表示し、「Save To File」->「Comma-separated Values (CSV)」を選択します。
「Save Data To File」ダイアログが表示されますので、src/test/resources/testdata/base の下に [テーブル名].csv のファイル名で保存します。
src/test/resources/testdata/base の下に table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。
この時点で src/test/resources は以下の画像の状態になります。
TestDataResource クラスの準備
UserInfoServiceTest クラスの作成
src/main/java/ksbysample/webapp/lending/service の下の UserInfoService.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/service の下に UserInfoServiceTest.java が作成されますので、リンク先のその1の内容 に変更します。
テストを実行してみます。UserInfoServiceTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UserInfoServiceTest' with Coverage」を選択します。
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [ksbysample.common.test.TestDataResource] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.junit.Rule(), @org.springframework.beans.factory.annotation.Autowired(required=true)}
というエラーメッセージが表示されて、テストは失敗しました。原因を調査します。原因は、今回からテストで利用するクラスを src/test/ksbysample/common/test の下に移動していたのですが、Spring Boot でデフォルトで ComponentScan の対象になるのは Application.java のあるパッケージ ( ksbysample.webapp.lending ) から下だけなので、ksbysample.common.test パッケージの下のクラスが ComponentScan の対象外になっていたためでした。
ksbysample パッケージの下全てが ComponentScan の対象になるようにします。src/main/ksbysample/webapp/lending の下の Application.java を リンク先の内容 に変更します。
追加した
@ComponentScan("ksbysample")
の左側に表示されるアイコンをクリックして ComponentScan されているクラスを表示すると、TestDataResource.java が含まれていることが確認できます。再度テストを実行します。今度は testIncCntBadcredentials メソッドだけテストが失敗しました。テスト結果を比較する値が Short ではなく Integer と判断されているためのようです。
テスト結果を比較する値が Short と判断されるようにします。src/test/java/ksbysample/webapp/lending/service の下の UserInfoServiceTest.java を リンク先のその2の内容 に変更します。
再度テストを実行し、今度は成功することが確認できました。
一旦ここまでで Git - Add した後、commit しておきます。
- Git - Add しているのはテストデータの CSV ファイルを追加するためです。
LendingUserDetailsTest クラスの作成
src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetails.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。
- 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。
src/test/java/ksbysample/webapp/lending/security の下に LendingUserDetailsTest.java が作成されますので、リンク先の内容 に変更します。
テストを実行してみます。LendingUserDetailsTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UserInfoServiceTe..' with Coverage」を選択します。
テストが全て成功することが確認できます。
commit します。
LendingUserDetailsServiceTest クラスの作成
src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetailsService.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。
- 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。
src/test/java/ksbysample/webapp/lending/security の下に LendingUserDetailsServiceTest.java が作成されますので、リンク先の内容 に変更します。
テストを実行してみます。LendingUserDetailsServiceTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LendingUserDetailsSe...' with Coverage」を選択します。
テストが全て成功することが確認できます。
commit します。
メモ書き
AssertJ を使ってみて
JUnit の assertThat の場合には使用できる Matcher を本や Web で調べましたが、AssertJ であれば補完機能でメソッド一覧が表示されるので調べる手間が減りそうです。
用意されているメソッドが分かりやすく、かつ使いやすい印象を受けました。公式サイトやサンプルも充実していると思います。Java 8 対応もされているのがいいですね。
ちょっと使ってみただけですが、個人的には JUnit の assertThat を使うより全然便利です。JavaのテストにはAssertJがオススメ の記事だと「びっくりするくらいマイナー」らしいですが、本当に使われていないのでしょうか?
ソースコード
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.thymeleaf.extras:thymeleaf-extras-springsecurity3") 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.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-redis") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("${jdbcDriver}") compile("org.seasar.doma:doma:2.3.1") 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.4") compile("com.google.guava:guava:18.0") compile("org.springframework.session:spring-session:1.0.1.RELEASE") testCompile("org.dbunit:dbunit:2.5.1") testCompile("com.icegreen:greenmail:1.4.1") testCompile("org.assertj:assertj-core:3.1.0") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.3.1") domaGenRuntime("${jdbcDriver}") }
testCompile("org.assertj:assertj-core:3.1.0")
を追加します。
table-ordering.txt
user_info user_role
TestDataResource.java
package ksbysample.common.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 String TESTDATA_DIR = "src/test/resources/testdata/base"; private final String BACKUP_FILE_NAME = "ksbylending_backup"; private final List<String> BACKUP_TABLES = Arrays.asList( "user_info" , "user_role" ); @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(TESTDATA_DIR)); 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(); } } }
private final String TESTDATA_DIR = "src/test/resources/testdata/base";
を追加し、before メソッド内ではテストデータの位置をこの定数で指定するように変更します。private final List<String> BACKUP_TABLES
にバックアップするテーブルとして user_info, user_role を列挙します。
UserInfoServiceTest.java
■その1
package ksbysample.webapp.lending.service; import ksbysample.common.test.TestDataResource; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.entity.UserInfo; 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 static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class UserInfoServiceTest { private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com"; @Rule @Autowired public TestDataResource testDataResource; @Autowired private UserInfoDao userInfoDao; @Autowired private UserInfoService userInfoService; @Test public void testIncCntBadcredentials() throws Exception { UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isZero(); userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO); UserInfo userInfo2 = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo2.getCntBadcredentials()).isEqualTo(userInfo.getCntBadcredentials() + 1); } @Test public void testInitCntBadcredentials() throws Exception { userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO); UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isNotZero(); userInfoService.initCntBadcredentials(MAILADDR_TANAKA_TARO); userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isZero(); } }
■その2
@Test public void testIncCntBadcredentials() throws Exception { UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isZero(); userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO); UserInfo userInfo2 = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); assertThat(userInfo2.getCntBadcredentials()).isEqualTo((short)(userInfo.getCntBadcredentials() + 1)); }
.isEqualTo(userInfo.getCntBadcredentials() + 1);
→.isEqualTo((short)(userInfo.getCntBadcredentials() + 1));
へ変更します。
Application.java
package ksbysample.webapp.lending; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ImportResource; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import java.io.Serializable; import java.text.MessageFormat; @ImportResource("classpath:applicationContext-${spring.profiles.active}.xml") @SpringBootApplication @ComponentScan("ksbysample") @EnableRedisHttpSession public class Application { public static void main(String[] args) { String springProfilesActive = System.getProperty("spring.profiles.active"); if (!StringUtils.equals(springProfilesActive, "product") && !StringUtils.equals(springProfilesActive, "develop") && !StringUtils.equals(springProfilesActive, "unittest")) { throw new UnsupportedOperationException(MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。", springProfilesActive)); } SpringApplication.run(Application.class, args); } }
@ComponentScan("ksbysample")
を追加します。
LendingUserDetailsTest.java
package ksbysample.webapp.lending.security; import ksbysample.common.test.TestDataResource; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.dao.UserRoleDao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.entity.UserRole; 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.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class LendingUserDetailsTest { private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com"; private final String MAILADDR_KATO_HIROSHI = "kato.hiroshi@sample.com"; @Rule @Autowired public TestDataResource testDataResource; @Autowired private UserInfoDao userInfoDao; @Autowired private UserRoleDao userRoleDao; @Test public void 利用可能でロックされておらず有効期限切れもないユーザでのテスト() { UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.addAll( userRoleList.stream() .map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, authorities); assertThat(lendingUserDetails.getUsername()).isEqualTo(userInfo.getMailAddress()); assertThat(lendingUserDetails.getPassword()).isEqualTo(userInfo.getPassword()); assertThat(lendingUserDetails.getName()).isEqualTo(userInfo.getUsername()); assertThat(lendingUserDetails.getAuthorities()).extracting("authority") .containsOnly("ROLE_USER", "ROLE_ADMIN", "ROLE_APPROVER"); assertThat(lendingUserDetails.isAccountNonExpired()).isTrue(); assertThat(lendingUserDetails.isAccountNonLocked()).isTrue(); assertThat(lendingUserDetails.isCredentialsNonExpired()).isTrue(); assertThat(lendingUserDetails.isEnabled()).isTrue(); } @Test public void 利用不可能でロック中で全て有効期限切れのユーザでのテスト() { UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_KATO_HIROSHI); LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, null); assertThat(lendingUserDetails.getUsername()).isEqualTo(userInfo.getMailAddress()); assertThat(lendingUserDetails.getPassword()).isEqualTo(userInfo.getPassword()); assertThat(lendingUserDetails.getName()).isEqualTo(userInfo.getUsername()); assertThat(lendingUserDetails.getAuthorities()).isNull(); assertThat(lendingUserDetails.isAccountNonExpired()).isFalse(); assertThat(lendingUserDetails.isAccountNonLocked()).isFalse(); assertThat(lendingUserDetails.isCredentialsNonExpired()).isFalse(); assertThat(lendingUserDetails.isEnabled()).isFalse(); } }
LendingUserDetailsServiceTest.java
package ksbysample.webapp.lending.security; import ksbysample.common.test.TestDataResource; import ksbysample.webapp.lending.Application; import org.assertj.core.api.ThrowableAssert; 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.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.StrictAssertions.assertThatThrownBy; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class LendingUserDetailsServiceTest { private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com"; private final String MAILADDR_TEST_TARO = "test.taro@sample.com"; @Rule @Autowired public TestDataResource testDataResource; @Autowired private UserDetailsService userDetailsService; @Autowired private MessageSource messageSource; @Test public void 存在するユーザならばUserDetailsが返る() { UserDetails userDetails = userDetailsService.loadUserByUsername(MAILADDR_TANAKA_TARO); assertThat(userDetails).isNotNull(); assertThat(userDetails.getUsername()).isEqualTo(MAILADDR_TANAKA_TARO); } @Test public void 存在しないユーザならばUsernameNotFoundExceptionが発生する() { assertThatThrownBy(new ThrowableAssert.ThrowingCallable() { @Override public void call() throws Throwable { UserDetails userDetails = userDetailsService.loadUserByUsername(MAILADDR_TEST_TARO); } }).isInstanceOf(UsernameNotFoundException.class) .hasMessage(messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , null, LocaleContextHolder.getLocale())); } }
履歴
2015/08/08
初版発行。