- 投稿日:2019-10-08T22:39:47+09:00
Hyperledger IrohaのJavaSDKを使ってみる
記事の内容
Hyperledger Irohaのドキュメントに書いてあるJavaSDKのサンプルコードを動かしてみました。
動かすまでにかなり時間がかかってしまったのでやった内容を残しておきます。環境
- JDK 1.8
- Hyperledger Iroha:1.0.0
環境構築
手順はこちらのドキュメントを参考にしました。
https://iroha.readthedocs.io/ja/latest/getting_started/index.html実装
eclipseでmavenプロジェクトを作成してサンプルコードを動かしました。
pom.xml
サンプルコードの前にかなり躓いた点ですが、コンパイルを通すまでにかなり苦戦しました。
いろいろ試した結果、以下のpom.xmlになりました。pom.xml<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <name>iroha</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.source>${java.version}</maven.compiler.source> </properties> <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> <repository> <id>mvn</id> <url>https://mvnrepository.com</url> </repository> </repositories> <dependencies> <!-- https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.22.1</version> </dependency> <!-- https://mvnrepository.com/artifact/io.grpc/grpc-netty --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty</artifactId> <version>1.22.1</version> </dependency> <!-- https://mvnrepository.com/artifact/io.grpc/grpc-stub --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.22.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>protoc-gen-grpc-java</artifactId> <version>1.12.0</version> </dependency> <dependency> <groupId>com.github.hyperledger</groupId> <artifactId>iroha-java</artifactId> <version>6.1.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/jp.co.soramitsu/iroha --> <dependency> <groupId>jp.co.soramitsu</groupId> <artifactId>iroha</artifactId> <version>0.0.8</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java-util</artifactId> <version>3.10.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.10.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.testinfected.hamcrest-matchers/core-matchers --> <dependency> <groupId>org.testinfected.hamcrest-matchers</groupId> <artifactId>core-matchers</artifactId> <version>1.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <compilerArgument>-Werror</compilerArgument> </configuration> </plugin> </plugins> </build> </project>躓いた点は2点です。
1点目は「protoc-gen-grpc-java」のjarファイルと何か依存関係があるみたいで、jarファイルが無いとエラーになりました。
依存関係を追加したものの、「protoc-gen-grpc-java」のmavenリポジトリにjarファイルが存在しませんでした。
色々試してみた結果、適当なjarファイルをリネームし、ローカルに配置することで解決しました。
どうやら、依存関係はあるものの呼び出されてはいないみたいです。2点目はirohaのjarの中でjunitの「TestRule」というクラスを参照しているみたいです。
このクラスはjunitのバージョン4.12から追加されたものらしく、バージョンを4.12以上にする必要がありました。サンプルコード
ドキュメントに載っているサンプルコードです。
Example1.javaimport iroha.protocol.BlockOuterClass; import iroha.protocol.Primitive.RolePermission; import java.math.BigDecimal; import java.security.KeyPair; import java.util.Arrays; import jp.co.soramitsu.crypto.ed25519.Ed25519Sha3; import jp.co.soramitsu.iroha.testcontainers.IrohaContainer; import jp.co.soramitsu.iroha.testcontainers.PeerConfig; import jp.co.soramitsu.iroha.testcontainers.detail.GenesisBlockBuilder; import lombok.val; public class Example1 { private static final String bankDomain = "bank"; private static final String userRole = "user"; private static final String usdName = "usd"; private static final Ed25519Sha3 crypto = new Ed25519Sha3(); private static final KeyPair peerKeypair = crypto.generateKeypair(); private static final KeyPair useraKeypair = crypto.generateKeypair(); private static final KeyPair userbKeypair = crypto.generateKeypair(); private static String user(String name) { return String.format("%s@%s", name, bankDomain); } private static final String usd = String.format("%s#%s", usdName, bankDomain); /** * <pre> * Our initial state cosists of: * - domain "bank", with default role "user" - can transfer assets and can query their amount * - asset usd#bank with precision 2 * - user_a@bank, which has 100 usd * - user_b@bank, which has 0 usd * </pre> */ private static BlockOuterClass.Block getGenesisBlock() { return new GenesisBlockBuilder() // first transaction .addTransaction( // transactions in genesis block can have no creator Transaction.builder(null) // by default peer is listening on port 10001 .addPeer("0.0.0.0:10001", peerKeypair.getPublic()) // create default "user" role .createRole(userRole, Arrays.asList( RolePermission.can_transfer, RolePermission.can_get_my_acc_ast, RolePermission.can_get_my_txs, RolePermission.can_receive ) ) .createDomain(bankDomain, userRole) // create user A .createAccount("user_a", bankDomain, useraKeypair.getPublic()) // create user B .createAccount("user_b", bankDomain, userbKeypair.getPublic()) // create usd#bank with precision 2 .createAsset(usdName, bankDomain, 2) // transactions in genesis block can be unsigned .build() // returns ipj model Transaction .build() // returns unsigned protobuf Transaction ) // we want to increase user_a balance by 100 usd .addTransaction( Transaction.builder(user("user_a")) .addAssetQuantity(usd, new BigDecimal("100")) .build() .build() ) .build(); } public static PeerConfig getPeerConfig() { PeerConfig config = PeerConfig.builder() .genesisBlock(getGenesisBlock()) .build(); // don't forget to add peer keypair to config config.withPeerKeyPair(peerKeypair); return config; } /** * Custom facade over GRPC Query */ public static int getBalance(IrohaAPI api, String userId, KeyPair keyPair) { // build protobuf query, sign it val q = Query.builder(userId, 1) .getAccountAssets(userId) .buildSigned(keyPair); // execute query, get response val res = api.query(q); // get list of assets from our response val assets = res.getAccountAssetsResponse().getAccountAssetsList(); // find usd asset val assetUsdOptional = assets .stream() .filter(a -> a.getAssetId().equals(usd)) .findFirst(); // numbers are small, so we use int here for simplicity return assetUsdOptional .map(a -> Integer.parseInt(a.getBalance())) .orElse(0); } public static void main(String[] args) { // for simplicity, we will create Iroha peer in place IrohaContainer iroha = new IrohaContainer() .withPeerConfig(getPeerConfig()); // start the peer. blocking call iroha.start(); // create API wrapper IrohaAPI api = new IrohaAPI(iroha.getToriiAddress()); // transfer 100 usd from user_a to user_b val tx = Transaction.builder("user_a@bank") .transferAsset("user_a@bank", "user_b@bank", usd, "For pizza", "10") .sign(useraKeypair) .build(); // create transaction observer // here you can specify any kind of handlers on transaction statuses val observer = TransactionStatusObserver.builder() // executed when stateless or stateful validation is failed .onTransactionFailed(t -> System.out.println(String.format( "transaction %s failed with msg: %s", t.getTxHash(), t.getErrOrCmdName() ))) // executed when got any exception in handlers or grpc .onError(e -> System.out.println("Failed with exception: " + e)) // executed when we receive "committed" status .onTransactionCommitted((t) -> System.out.println("Committed :)")) // executed when transfer is complete (failed or succeed) and observable is closed .onComplete(() -> System.out.println("Complete")) .build(); // blocking send. // use .subscribe() for async sending api.transaction(tx) .blockingSubscribe(observer); /// now lets query balances val balanceUserA = getBalance(api, user("user_a"), useraKeypair); val balanceUserB = getBalance(api, user("user_b"), userbKeypair); // ensure we got correct balances assert balanceUserA == 90; assert balanceUserB == 10; } }とりあえず、動かしてみます。
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport [main] INFO org.testcontainers.dockerclient.DockerMachineClientProviderStrategy - Found docker-machine, and will use machine named default [main] INFO org.testcontainers.dockerclient.DockerMachineClientProviderStrategy - Docker daemon IP address for docker machine default is 192.168.99.100 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were: [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - NpipeSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null) [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - WindowsClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause ConnectException (Connection refused: connect) [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - DockerMachineClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause ConnectException (Connection refused: connect) [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - As no valid configuration was found, execution cannot continue Exception in thread "main" java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configurationはい。エラーがでました。
「Could not find a valid Docker environment.」
このエラーですが、どうもJavaからDockerを操作しようとして「Dockerfile」が存在しない場合にこのエラーが出るみたいです。私はDockerではなく、VirtualBoxで環境を作っているので動く訳がありませんでした。
ここでプログラムをちゃんと読んでみると、Peerを追加して、追加したPeerにアカウントを作成し、Asset(トークン)の送信を行っています。
main文の処理からDockerを操作する部分を消し、ユーザーもデフォルトで作成されている、「admin@test」と「test@test」に変更して動かしてみます。
Example1.javapublic static void main(String[] args) throws Exception{ URI uri = new URI(null,null, "IPアドレス",50051,null,null,null); // create API wrapper IrohaAPI api = new IrohaAPI(uri); // transfer 100 usd from user_a to user_b val tx = Transaction.builder("admin@test") .transferAsset("admin@test", "test@test", usd, "For pizza", "10") .sign(useraKeypair) .build(); // create transaction observer // here you can specify any kind of handlers on transaction statuses val observer = TransactionStatusObserver.builder() // executed when stateless or stateful validation is failed .onTransactionFailed(t -> System.out.println(String.format( "transaction %s failed with msg: %s", t.getTxHash(), t.getErrOrCmdName() ))) // executed when got any exception in handlers or grpc .onError(e -> System.out.println("Failed with exception: " + e)) // executed when we receive "committed" status .onTransactionCommitted((t) -> System.out.println("Committed :)")) // executed when transfer is complete (failed or succeed) and observable is closed .onComplete(() -> System.out.println("Complete")) .build(); // blocking send. // use .subscribe() for async sending api.transaction(tx) .blockingSubscribe(observer); /// now lets query balances val balanceUserA = getBalance(api, user("user_a"), useraKeypair); val balanceUserB = getBalance(api, user("user_b"), userbKeypair); // ensure we got correct balances assert balanceUserA == 90; assert balanceUserB == 10; }修正したポイントとしては「IrohaApi」クラスのコンストラクタです。
このコンストラクタはURIクラスを受け取りますが、コンストラクタの中で受け取ったURIクラスからドメインとポートを取得しているだけです。
なので、ドメインとポートだけを設定したURIクラスを作成し、引数に設定します。
ここまで出来たら実行します。transaction c3d966d1cbc521b99e1cb60cbe444f129947bdfbe348abe7b7262dd81079c505 failed with msg: signatures validation Completeエラーは出てますが、とりあえず動きました。
このエラーの原因はトランザクション署名に使用したKeyPairがadmin@testのものではないことが原因です。原因は分かっていますが、まだ対応方法を調べている最中なので、分かり次第更新します。
2019/10/09 追記
Transaction送信の方法が分かったので追記します。
Example1.javapublic static void main(String[] args) throws Exception{ URI uri = new URI(null,null, "192.168.33.10",50051,null,null,null); // create API wrapper IrohaAPI api = new IrohaAPI(uri); byte[] pubByte = Hex.decodeHex("admin@test.pubの中身(公開鍵)"); byte[] privByte = Hex.decodeHex("test@test.privの中身(秘密鍵)"); KeyPair adminKeyPair = Ed25519Sha3.keyPairFromBytes(privByte, pubByte); // transfer 100 usd from user_a to user_b val tx = Transaction.builder("admin@test") .transferAsset("admin@test", "test@test", usd, "For pizza", "10") .sign(adminKeyPair) .build(); // create transaction observer // here you can specify any kind of handlers on transaction statuses val observer = TransactionStatusObserver.builder() // executed when stateless or stateful validation is failed .onTransactionFailed(t -> System.out.println(String.format( "transaction %s failed with msg: %s", t.getTxHash(), t.getErrOrCmdName() ))) // executed when got any exception in handlers or grpc .onError(e -> System.out.println("Failed with exception: " + e)) // executed when we receive "committed" status .onTransactionCommitted((t) -> System.out.println("Committed :)")) // executed when transfer is complete (failed or succeed) and observable is closed .onComplete(() -> System.out.println("Complete")) .build(); // blocking send. // use .subscribe() for async sending api.transaction(tx) .blockingSubscribe(observer); /// now lets query balances val balanceUserA = getBalance(api, user("user_a"), useraKeypair); val balanceUserB = getBalance(api, user("user_b"), userbKeypair); // ensure we got correct balances assert balanceUserA == 90; assert balanceUserB == 10; }ポイントとしては公開鍵と秘密鍵からKeyPairのオブジェクトを作成します。
byte[] pubByte = Hex.decodeHex("admin@test.pubの中身(公開鍵)"); byte[] privByte = Hex.decodeHex("test@test.privの中身(秘密鍵)"); KeyPair adminKeyPair = Ed25519Sha3.keyPairFromBytes(privByte, pubByte);これをsignメソッドの引数に設定すれば完了です。
実行してみます。Committed :) Complete正常に終了しました。
それではブロックの中身を見てみます。
公式ドキュメントの手順で環境構築をすると「/tmp/block_store」ディレクトリに出力されているはずです。
中身は整形したJSONです。{ "blockV1": { "payload": { "transactions": [ { "payload": { "reducedPayload": { "commands": [ { "transferAsset": { "srcAccountId": "admin@test", "destAccountId": "test@test", "assetId": "usd#bank", "description": "For pizza", "amount": "10" } } ], "creatorAccountId": "admin@test", "createdTime": "1570624170543", "quorum": 1 } }, "signatures": [ { "publicKey": "313A07E6384776ED95447710D15E59148473CCFC052A681317A72A69F2A49910", "signature": "4B2B45A4F2FDB9A7DE6F30E110D8DEA5E5AAB30C40F5685CFA71FDC38E72BF3839954DDA13FE027FEA18DA9F97332E5E265822922204D38F1667D60E5F8E9601" } ] } ], "height": "17", "prevBlockHash": "ff33c8483725758e09583e7f670b9c37a091357bc027602a72452d44907861f1", "createdTime": "1570624184188" }, "signatures": [ { "publicKey": "bddd58404d1315e0eb27902c5d7c8eb0602c16238f005773df406bc191308929", "signature": "680ee97a530314e877ee7518b22975607e328a232a60f963029cb07b18b9101d0de6bf174ecd07ab29bac8123e8c47f12761835a121b301f192c0de9a908670e" } ] } }無事ブロックに取り込まれたことを確認できました。
感想
最近ethereumを勉強していたので、マイニングをしなくてもブロックに取り込まれるところが、うん?ってなりましたが、考えてみたら当たり前ですね。
今回のサンプルプログラムを動かすにしても、やはり日本語の情報がとても少ないという印象を受けました。
簡単なウォレットアプリの作成やチェーンコードのデプロイなどやっていこうと思うので積極的に情報を残していければなと思います。
- 投稿日:2019-10-08T17:42:47+09:00
【Azure】無料でJava アプリを作成してみた【初心者】
この記事ではAzureを使用して、java の Web Appを無料で作成していきます。
Azure サブスクリプションをお持ちでない場合は、
開始する前に無料アカウントを作成してください。
目標
Web App作成 → FTPで接続テスト → Git連携 → SQL Serverに連携App Serviceを作成する
・ブラウザーでポータルを開く
・「ホーム > App Service」で「追加」を選択
・項目を入力選択していく。
※無料会員で作成する場合、プランに気をつけないと金額かかる場合があります。
・内容に間違いが無いなら「確認と作成」を選択
・デプロイが完了したら、リソースに移動
・概要の「https://< appName >.azurewebsites.net」と表示されているURLをクリック
以下の画像のように表示されていたらOK!
※ 2019年以降でデザインがページ内容が変更されている可能性もある
・作成したサイトに表示されているQuickstartをクリック
・ここからは、公式ドキュメント「クイックスタート > Java アプリの作成」の通りに作業していく
Azure Cloud Shell を使用する
・Cloud Shell を起動させます。(ポータルの上にがあるのでそれを開く)
Java アプリを作成する
Cloud Shell プロンプトで次の Maven コマンドを実行して、helloworld という名前の新しいアプリを作成
※ Cloud Shell(黒画面)の左上が「Bash」以外になっていたら「Bash」に変更
bash// helloworld のディレクトリが存在する確認 //(存在するとmvn archetypeでエラーになる) ls -a // 新しいアプリを作成 mvn archetype:generate -DgroupId=example.demo -DartifactId=helloworld -DarchetypeArtifactId=maven-archetype-webapp // helloworld が作成されているか確認 ls -aMaven プラグインを構成する
helloworld ディレクトリ内のプロジェクト pom.xml ファイルを開きます。
bash// 'helloworld'ディレクトリに遷移 cd helloworld // pom.xml ファイルを開く code pom.xml次に、
pom.xml
ファイルの 要素内に次のプラグイン定義を追加します。pom.xml<plugins> <!--*************************************************--> <!-- Deploy to Tomcat in App Service Linux --> <!--*************************************************--> <plugin> <groupId>com.microsoft.azure</groupId> <artifactId>azure-webapp-maven-plugin</artifactId> <version>1.7.0</version> </plugin> </plugins>次に、デプロイを構成し、[コマンド プロンプト] で
maven コマンドmvn azure-webapp:config
を実行し、
Confirm (Y/N) プロンプトが表示されるまで
Enter キーを押して既定の構成を使用し、y
キーを押して構成を完了します。bashmvn azure-webapp:config // 以下が表示される [INFO] Scanning for projects... [INFO] [INFO] ----------------------< example.demo:helloworld >----------------------- [INFO] Building helloworld Maven Webapp 1.0-SNAPSHOT [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- azure-webapp-maven-plugin:1.7.0:config (default-cli) @ helloworld --- [WARNING] The plugin may not work if you change the os of an existing webapp. Define value for OS(Default: Linux): 1. linux [*] 2. windows 3. docker Enter index to use: Define value for javaVersion(Default: jre8): 1. jre8 [*] 2. java11 Enter index to use: Define value for runtimeStack(Default: TOMCAT 8.5): 1. TOMCAT 9.0 2. jre8 3. TOMCAT 8.5 [*] 4. WILDFLY 14 Enter index to use: Please confirm webapp properties AppName : helloworld-1558400876966 ResourceGroup : helloworld-1558400876966-rg Region : westeurope PricingTier : Premium_P1V2 OS : Linux RuntimeStack : TOMCAT 8.5-jre8 Deploy to slot : false Confirm (Y/N)? : Yもう一度 pom.xml に移動してプラグイン構成が更新されていることを確認します。
必要に応じて、pom ファイルで App Service の他の構成を直接変更できます。
その一般的なものを次に示します。※ 最初に設定したプランやApp名などを確認する(変更しれている場合がある)
<pricingTier>
<appName>
<resourceGroup>
は要確認!!!bashcode pom.xmlアプリケーションのデプロイ
次のコマンドを使用して、Java アプリを Azure にデプロイします。
bashmvn package azure-webapp:deployデプロイが完了したら、Web ブラウザーで次の URL を使用して、デプロイされたアプリケーションを参照します
「https://< appName >.azurewebsites.net」に
Hello World!
と表示されてればOK!
!!! 完了 !!!
【Azure】シリーズ
(記事作成するかも・・・)
・【Azure】「FTP」で「App Service」に接続してTOPの内容を変更する引用
【公式】Linux での Java Web アプリの作成 - Azure App Service | Microsoft Docs
- 投稿日:2019-10-08T17:42:47+09:00
【Azure】無料でJava アプリを作成してみた〜Web App作成 編〜【初心者】
◇ 概要
この記事ではAzureを使用して、java の Web Appを無料で作成していきます。
Azure サブスクリプションをお持ちでない場合は、
開始する前に無料アカウントを作成してください。【Azure】無料でJava アプリを作成してみた
【Azure】無料でJava アプリを作成してみた〜Web App作成 編〜【初心者】 今ここ
・「App Service」を使用して「java」Webアプリを作る
・TOPページに「Hello world!!」まで表示させる【Azure】無料でJava アプリを作成してみた〜FTPで接続 編〜【初心者】
・前回作成した「java」WebアプリにFTPで接続する
・TOPページの「Hello world!!」を変更する〜 次回以降の予定 〜
【Azure】無料でJava アプリを作成してみた〜Git 編〜【初心者】
【Azure】無料でJava アプリを作成してみた〜SQL Server 編〜【初心者】◇ App Serviceを作成する
・ブラウザーでポータルを開く
・「ホーム > App Service」で「追加」を選択
・項目を入力選択していく。
※無料会員で作成する場合、プランに気をつけないと金額かかる場合があります。
・内容に間違いが無いなら「確認と作成」を選択
・デプロイが完了したら、リソースに移動
・概要の「https://< appName >.azurewebsites.net」と表示されているURLをクリック
以下の画像のように表示されていたらOK!
※ 2019年以降でデザインがページ内容が変更されている可能性もある
・作成したサイトに表示されているQuickstartをクリック
◇ Java アプリの作成 ※ 公式ドキュメント参考
公式ドキュメント「クイックスタート > Java アプリの作成」を以下の通りに作業していく。
【公式】Linux での Java Web アプリの作成 - Azure App Service | Microsoft Docs※ 公式のドキュメントに変更される可能性があるため、補助程度で認識お願いいたします!
◆ Azure Cloud Shell を使用する
・Cloud Shell を起動させます。(ポータルの上にがあるのでそれを開く)
◆ Java アプリを作成する
Cloud Shell プロンプトで次の Maven コマンドを実行して、helloworld という名前の新しいアプリを作成
※ Cloud Shell(黒画面)の左上が「Bash」以外になっていたら「Bash」に変更
bash// helloworld のディレクトリが存在する確認 //(存在するとmvn archetypeでエラーになる) ls -a // 新しいアプリを作成 mvn archetype:generate -DgroupId=example.demo -DartifactId=helloworld -DarchetypeArtifactId=maven-archetype-webapp // helloworld が作成されているか確認 ls -a◆ Maven プラグインを構成する
helloworld ディレクトリ内のプロジェクト pom.xml ファイルを開きます。
bash// 'helloworld'ディレクトリに遷移 cd helloworld // pom.xml ファイルを開く code pom.xml次に、
pom.xml
ファイルの 要素内に次のプラグイン定義を追加します。pom.xml<plugins> <!--*************************************************--> <!-- Deploy to Tomcat in App Service Linux --> <!--*************************************************--> <plugin> <groupId>com.microsoft.azure</groupId> <artifactId>azure-webapp-maven-plugin</artifactId> <version>1.7.0</version> </plugin> </plugins>次に、デプロイを構成し、[コマンド プロンプト] で
maven コマンドmvn azure-webapp:config
を実行し、
Confirm (Y/N) プロンプトが表示されるまで
Enter キーを押して既定の構成を使用し、y
キーを押して構成を完了します。bashmvn azure-webapp:config // 以下が表示される [INFO] Scanning for projects... [INFO] [INFO] ----------------------< example.demo:helloworld >----------------------- [INFO] Building helloworld Maven Webapp 1.0-SNAPSHOT [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- azure-webapp-maven-plugin:1.7.0:config (default-cli) @ helloworld --- [WARNING] The plugin may not work if you change the os of an existing webapp. Define value for OS(Default: Linux): 1. linux [*] 2. windows 3. docker Enter index to use: Define value for javaVersion(Default: jre8): 1. jre8 [*] 2. java11 Enter index to use: Define value for runtimeStack(Default: TOMCAT 8.5): 1. TOMCAT 9.0 2. jre8 3. TOMCAT 8.5 [*] 4. WILDFLY 14 Enter index to use: Please confirm webapp properties AppName : helloworld-1558400876966 ResourceGroup : helloworld-1558400876966-rg Region : westeurope PricingTier : Premium_P1V2 OS : Linux RuntimeStack : TOMCAT 8.5-jre8 Deploy to slot : false Confirm (Y/N)? : Yもう一度 pom.xml に移動してプラグイン構成が更新されていることを確認します。
必要に応じて、pom ファイルで App Service の他の構成を直接変更できます。
その一般的なものを次に示します。※ 最初に設定したプランやApp名などを確認する(変更しれている場合がある)
<pricingTier>
<appName>
<resourceGroup>
は要確認!!!bashcode pom.xml◆ アプリケーションのデプロイ
次のコマンドを使用して、Java アプリを Azure にデプロイします。
bashmvn package azure-webapp:deployデプロイが完了したら、Web ブラウザーで次の URL を使用して、デプロイされたアプリケーションを参照します
「https://< appName >.azurewebsites.net」に
Hello World!
と表示されてればOK!
!!! 完了 !!!
関連記事
- 【Azure】無料でJava アプリを作成してみた〜Web App作成 編〜【初心者】
- 【Azure】無料でJava アプリを作成してみた〜FTPで接続 編〜【初心者】
- 【Azure】無料でJava アプリを作成してみた〜Git 編〜【初心者】
- 【Azure】無料でJava アプリを作成してみた〜SQL Server 編〜【初心者】
引用
【公式】Linux での Java Web アプリの作成 - Azure App Service | Microsoft Docs
- 投稿日:2019-10-08T17:04:39+09:00
dockerでkotlinをcliでコンパイル、javaで実行できる環境をつくった
はじめてkotlinを勉強しようと思ったのですが、macにkotlinコンパイラをいれるのも少し嫌だったので、docker composeで一式つくりました。
ビルドツールを使用することは想定しておらず、kotlinの簡単な文法を動かしながら確認できる環境を用意するというコンセプトのもと作成してます。
必要な方は参考にしてみてください。できたもの
./docker-compose.yml
version: "3.7" services: java: build: ./docker/java tty: true volumes: - ./src:/usr/local/src./docker/java/Dockerfile
FROM openjdk:13-slim RUN apt-get update && apt-get install -y git \ unzip \ zip \ curl \ vim WORKDIR /usr/local/src RUN curl -s https://get.sdkman.io | bash RUN /bin/bash -l -c "chmod a+x $HOME/.sdkman/bin/sdkman-init.sh;$HOME/.sdkman/bin/sdkman-init.sh;sdk install kotlin" CMD ["/bin/bash"]ディレクトリ構成は以下のようになってます
. ├── docker │ └── java │ └── Dockerfile ├── docker-compose.yml └── src └── test.ktつかいかた
下準備
docker-compose.ymlを配置したディレクトリで、以下のコマンドを実行するのみです。
$ docker-compose build $ docker-compose upうごかす
実際に、
test.kt
をコンパイルして、java
コマンドで実行します。
コンパイルする対象は./src/test.kt
です。fun main(args: Array<String>) { println("Hello, World!") }コンテナに入って、コンパイルして、実行します。
$ docker-compose exec java bash root@3b62768acd0b:/usr/local/src# kotlinc test.kt -include-runtime -d test.jar OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release. root@3b62768acd0b:/usr/local/src# java -jar test.jar Hello, World! root@3b62768acd0b:/usr/local/src#
-Xverify:none
は駄目だというwarningがでてしまいますが、コンパイルができて、javaコマンドで実行することができました。さらに、
/usr/local/src
をボリュームマウントしてますので、コンパイル後にできたjarファイルは、ホストマシンのファイルシステムでも確認可能です。. ├── docker │ └── java │ └── Dockerfile ├── docker-compose.yml └── src ├── test.jar └── test.ktさいごに
これから勉強を進めていく中で、ビルドツール使いたくなったり、いろいろ不都合がでてくるかもしれませんが、一旦環境が整ったということで、どんどん勉強していきたいです!
- 投稿日:2019-10-08T16:52:06+09:00
HashMap#putAll()の挙動がJava7とJava8で異なる
こんなコードがあった。
HashMapを継承したカスタムHashMapクラス@Override public Object put(Object key, Object value) { // キーを小文字にする return super.put(key.toString().toLowerCase(), value); }カスタムHashMapを使用しているコードCustomHashMap map = new CustomHashMap(); map.putAll(valueMap); // Java7ではキーがすべて小文字になるJava7からJava8に上げると、mapのキーが小文字にならずに、今まで通りの挙動をしなくなった。
HashMapのコードを読んだところ、
- Java7では、putAll()からput()を呼び出している
- Java8では、putAll()からput()を呼び出していない
ことが原因と判明。つまり、Overrideしたputメソッドを呼び出していないためである。
putAllはOverrideしていなかったので、Java7ではたまたま動いていた、と考えるべきか。
(Java7の実装を読んでいたのかも知れないが。)
- 投稿日:2019-10-08T14:04:26+09:00
初学者が最初にやったJava(while文)基本問題
最初にやったJavaのwhile文問題のアウトプット
問1
サイコロを2個ふるよぞろ目が何回目にでるかな?
[実行例]
1回目(2,3) 2回目(3,2) 3回目(4,4) 3回目にぞろ目がでました!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Random rand=new Random(); int count=0; while(true){ count++; int dice1=rand.nextInt(6)+1; int dice2=rand.nextInt(6)+1; System.out.printf("%d回目(%d,%d)%n",count,dice1,dice2); if(dice1 == dice2){ break; } } System.out.println(count+"回目にぞろ目がでました!"); } }
問2
以下の処理を実現せよ。
[実行例]
9
8
7
6
5
4
3
2
1
発射!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ int n=9; while(n>0){ System.out.println(n); n--; } System.out.println("発射!"); } }
問3
1~999までの乱数を繰り返し生成し777が最初に出るまでの回数を表示せよ。
[実行例]
1:34 2:432 3:321 ..... 1034:777 1034回目に777がでました!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Random rand=new Random(); int num=-1; int count=0; while(num !=777){ count++; num=rand.nextInt(999)+1; System.out.printf("%d:%d%n",count,num); } System.out.println(count+"回目に777がでました!"); } }
問4
[実行結果]
正の整数>20
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Scanner sc=new Scanner(System.in); System.out.print("正の整数>"); int max=sc.nextInt(); int i=1; String ans; while(i<=max){ if(i %3 == 0 && i % 5== 0){ ans="FizzBuzz"; }else if(i % 3==0){ ans="Fizz"; }else if(i % 5==0){ ans="Buzz"; }else{ ans=i+""; } System.out.println(ans); i++; } } }って認識です。
- 投稿日:2019-10-08T14:04:26+09:00
Java(while文)基本問題
最初にやったJavaのwhile文問題のアウトプット
問1
サイコロを2個ふるよぞろ目が何回目にでるかな?
[実行例]
1回目(2,3) 2回目(3,2) 3回目(4,4) 3回目にぞろ目がでました!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Random rand=new Random(); int count=0; while(true){ count++; int dice1=rand.nextInt(6)+1; int dice2=rand.nextInt(6)+1; System.out.printf("%d回目(%d,%d)%n",count,dice1,dice2); if(dice1 == dice2){ break; } } System.out.println(count+"回目にぞろ目がでました!"); } }
問2
以下の処理を実現せよ。
[実行例]
9
8
7
6
5
4
3
2
1
発射!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ int n=9; while(n>0){ System.out.println(n); n--; } System.out.println("発射!"); } }
問3
1~999までの乱数を繰り返し生成し777が最初に出るまでの回数を表示せよ。
[実行例]
1:34 2:432 3:321 ..... 1034:777 1034回目に777がでました!
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Random rand=new Random(); int num=-1; int count=0; while(num !=777){ count++; num=rand.nextInt(999)+1; System.out.printf("%d:%d%n",count,num); } System.out.println(count+"回目に777がでました!"); } }
問4
[実行結果]
正の整数>20
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
[解答例]
import java.util.*; public class Main{ public static void main(String[] args){ Scanner sc=new Scanner(System.in); System.out.print("正の整数>"); int max=sc.nextInt(); int i=1; String ans; while(i<=max){ if(i %3 == 0 && i % 5== 0){ ans="FizzBuzz"; }else if(i % 3==0){ ans="Fizz"; }else if(i % 5==0){ ans="Buzz"; }else{ ans=i+""; } System.out.println(ans); i++; } } }って認識です。
- 投稿日:2019-10-08T13:54:07+09:00
汎用性の高い処理を作る
汎用性ってむずかしい
入力された文字列を「,」で区切って配列にして個数を数えて100倍してコンソールに出力する処理
は?.javapackage ha; import java.util.Arrays; public class Trash { private static final String HA_IMIWAKANNAI = ",asd,asdadas,asda,sdas,asdasd,,asdasd,,,asd,"; public static void main(String[] args) { trash(HA_IMIWAKANNAI); } private static void trash(String str) { System.out.println(100 * Arrays.asList(str.split(",")).size()); } }意味わかんないというか何の役に立つのかわからん
汎用性のかけらもないよね
いつつかうんこれ
てことで
1つの処理でとめよう!
入力された文字列を「,」で区切って
配列にして
個数を数えて
100倍して
コンソールに出力する処理
改変.javapackage ha; import java.util.Arrays; import java.util.List; public class Trash { private static final String HA_IMIWAKANNAI = ",asd,asdadas,asda,sdas,asdasd,,asdasd,,,asd,"; public static void main(String[] args) { trash(HA_IMIWAKANNAI); } private static void trash(String str) { // System.out.println(100 * Arrays.asList(str.split(",")).size()); sysout(hundred(count(toArray(separate(str))))); } // 分割 private static String[] separate(Object obj) { return obj.toString().split(","); } // リストに private static List<String> toArray(String[] str) { return Arrays.asList(str); } // 数える private static int count(List<String> list) { return list.size(); } // 100倍 private static Integer hundred(int num) { return num * 100; } // 出力 private static void sysout(Object obj) { System.out.println(obj); } }分けすぎわろた
まぁ汎用性は上がってますよね
一部、他でも使えると思うので!StringUtilを自作する人も多いんじゃないでしょうか
Object型
これ便利よね
IteretorとかTとかSetとか、初心者はまだ知らないやつ、ほんと便利なので調べてみてほしいですforをより汎用的にしたのがiterator
順番とか気を付けたListやMapなどのCollectionsがSet
なんでもいい型がT
Objectもなんでもおkなクラスどうしても型の縛りで同じ処理を複数作らされたりとかする時は
是非こいつらを使って一括で行けないか考えてみましょただ下手にOptionalを使ってnullを許容してしまうと
事故になりかねないので、注意しましょうね汎用性ってスゴイ
- 投稿日:2019-10-08T12:03:43+09:00
Oracle Certified Java Programmer, Gold SE 8 を受けて
今更ですがようやくSJC-PからOracle資格へのアップグレードをしました。
試験を受けた動機は会社から「取あっておきましょうね!」と言われたから。
個人的にもJava1.5の知識しかなかったので、Java8の勉強 + 複数復習を兼ねて取り組みました。結果としては・・・
勉強期間:1か月(平日。勤務時間すべてを使ってました。)
結果:なんとか合格できた感じ。受けた試験
Upgrade to Java SE 8 OCP ( Java SE 6 and all prior versions)
SJC-Pからのアップグレードだから。やったこと
1.黒本で勉強
黒本で問題を解く⇒解説を理解する⇒総合問題を解く⇒解説を理解する。⇒DL総合問題を解く⇒解説を理解する。を2回繰り返しました。
2回目の結果がかなりいい線まで行ったので、これで完璧じゃね?と錯覚。
けれども、ネットでは「紫本」というワードがチラホラあったので油断せず、紫本を買うことにしました。2.紫本で勉強
紫本を渋々購入し、黒本でついた力を試すため、即行、総合問題をやりました。
結果・・・悲惨
自分ができている!理解してる!ことが実はごくごく一部であることを思い知らされました。
黒本と紫本は全く同じ構成なのに、問題の出し方が違います。
自分の抜けている部分と理解できていない点が、明らかになったのでほんとうに買ってよかったです。3.APIを見る
MapやListなど、お馴染みクラスもJava8で機能が追加されていますので、読みました 。
天才ではないので、ノートに一つ一つ書きましたが、完全に覚えきれていません・・・。4.Oracleのキャンペーン問題を解く
かなり助けられました。
キャンペーン問題と同じ内容が少し試験にも出てくれました。。。5.試し実行する
StreamやNIO2に関しては、私とってははじめて出会った技術なのでとにかく
試し実行をしました。試し実行すると、頭で理解しにくいことがスンナリ入ってくるので
環境がある方は是非!いざ申し込み!
めちゃくちゃ戸惑いました・・・。
ムズイ・・・。
初めてのピアソンVUE。わけわからずイライラが止まりませんでした・・・。
こちらを参考にがんばって申し込みます。
オラクル認定Javaプログラマ試験の申し込み方法と合格証書・領収書の取得方法※なお、私は受験6日前にピアソンVUEのアカウントを作りました・・・。
実際の試験
さすがに変なひっかけは無かったように思いますが、日本語が理解できない問題もあり
四苦八苦しました。
Stream、NIO2、try-with-resource、Fork/Join、ローカライズなどがよく出ました。
こんなニッチな
メソッドわかんない!!と心で叫んだ問題もちらほらありました・・・。試験の結果を見る
メールが5分くらい経ってやってきます。
メールにCertViewへのリンクがあるのでそれをクリックしてCertViewへログインします。
この段階で私は、Oracleアカウントがないことに気づき、急いでアカウントを作ります。
そのせいなのか、CertViewにログインができず、1時間あけたらなんとかログインして結果が見れた感じです。
(事前にちゃんとやっておけば良かったと後悔しました・・・)感想
1Z0-813の試験と1Z0-809では試験の内容が違いますが、1Z0-809の勉強をすることで
Java8の新機能やそれ以前の重要新機能の勉強をすることができたのでとても有意義でした。Java8の勉強をしないとなぁ・・・。
とふんわり思っていた時は、何をどう勉強すればよいのかわからず、結局勉強せずでしたが
今回の1Z0-813の資格勉強を通して、体系的に学べたと思います!
- 投稿日:2019-10-08T11:23:07+09:00
SpringSecurity(securityConfig)によるログイン機能実装
SpringSecurity(securityConfig)を使ったログイン認証機能実装
手順
1.pom.xmlへの追加
2.securityConfigクラスの作成
3.UserDetailsServiceImpl:サービスクラス作成
4.LoginUserドメインの作成
5.userのRepositoryクラス作成1.pom.xmlへの追加
以下2つのをタグの中に追加
pom.xml<!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>2.securityConfigクラスの作成
securityConfig.java//importは省略 @Configuration // 設定用のクラス @EnableWebSecurity // Spring Securityのウェブ用の機能を利用する public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService memberDetailsService; /** * このメソッドをオーバーライドすることで、 * 特定のリクエストに対して「セキュリティ設定」を * 無視する設定など全体にかかわる設定ができる. * 具体的には静的リソースに対してセキュリティの設定を無効にする。 * * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.WebSecurity) */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers( "/css/**" , "/img/**" , "/js/**" , "/fonts/**"); } /** * このメソッドをオーバーライドすることで、認可の設定やログイン/ログアウトに関する設定ができる. * * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity) */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認可に関する設定 .antMatchers("/","/login","/excuteLogin","/login/register").permitAll() //「/」などのパスは全てのユーザに許可 .antMatchers().hasRole("ADMIN") // /admin/から始まるパスはADMIN権限でログインしている場合のみアクセス可(権限設定時の「ROLE_」を除いた文字列を指定) .antMatchers().hasRole("USER") //.antMatchers("/admin/**").hasRole("ADMIN") // /admin/から始まるパスはADMIN権限でログインしている場合のみアクセス可(権限設定時の「ROLE_」を除いた文字列を指定) //.antMatchers("/user/**").hasRole("USER") // /user/から始まるパスはUSER権限でログインしている場合のみアクセス可(権限設定時の「ROLE_」を除いた文字列を指定) .anyRequest().authenticated(); // それ以外のパスは認証が必要 http.formLogin() // ログインに関する設定 .loginPage("/login") // ログイン画面に遷移させるパス(ログイン認証が必要なパスを指定してかつログインされていないとこのパスに遷移される) .loginProcessingUrl("/excuteLogin") // ログインボタンを押した際に遷移させるパス(ここに遷移させれば自動的にログインが行われる) .failureUrl("/login?error=true") //ログイン失敗に遷移させるパス .defaultSuccessUrl("/", true) // 第1引数:デフォルトでログイン成功時に遷移させるパス // 第2引数: true :認証後常に第1引数のパスに遷移 // false:認証されてなくて一度ログイン画面に飛ばされてもログインしたら指定したURLに遷移 .usernameParameter("email") // 認証時に使用するユーザ名のリクエストパラメータ名(今回はメールアドレスを使用) .passwordParameter("password"); // 認証時に使用するパスワードのリクエストパラメータ名 http.logout() // ログアウトに関する設定 .logoutRequestMatcher(new AntPathRequestMatcher("/logout**")) // ログアウトさせる際に遷移させるパス .logoutSuccessUrl("/login") // ログアウト後に遷移させるパス(ここではログイン画面を設定) .deleteCookies("JSESSIONID") // ログアウト後、Cookieに保存されているセッションIDを削除 .invalidateHttpSession(true); // true:ログアウト後、セッションを無効にする false:セッションを無効にしない } /** * 「認証」に関する設定.<br> * 認証ユーザを取得する「UserDetailsService」の設定や<br> * パスワード照合時に使う「PasswordEncoder」の設定 * * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder) */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(memberDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } /** * <pre> * bcryptアルゴリズムでハッシュ化する実装を返します. * これを指定することでパスワードハッシュ化やマッチ確認する際に * @Autowired * private PasswordEncoder passwordEncoder; * と記載するとDIされるようになります。 * </pre> * @return bcryptアルゴリズムでハッシュ化する実装オブジェクト */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }3.UserDetailsServiceImplの作成
ブラウザから入力されたメールアドレスを基にユーザー情報を取得しログイン認証をするためのサービスクラス。
ソースコードは以下の通り。UserDetailsServiceImpl.java@Service public class UserDetailsServiceImpl implements UserDetailsService { /** DBから情報を得るためのリポジトリ */ @Autowired private UserRepository userRepository; /* * (non-Javadoc) * * @see org.springframework.security.core.userdetails.UserDetailsService# * loadUserByUsername(java.lang.String) DBから検索をし、ログイン情報を構成して返す。 */ @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByMailAddress(email); if (user == null) { throw new UsernameNotFoundException("そのEmailは登録されていません。"); } // 権限付与の例 Collection<GrantedAuthority> authorityList = new ArrayList<>(); authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); // ユーザ権限付与 // if(member.isAdmin()) { // authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 管理者権限付与 // } return new LoginUser(user, authorityList); } }4.LoginUserドメインの作成
LoginUser.java/** * 管理者のログイン情報を格納するエンティティ. * */ public class LoginUser extends User { private static final long serialVersionUID = 1L; /** ユーザー情報 */ private final com.example.domain.User user; /** * 通常の管理者情報に加え、認可用ロールを設定する。 * * @param Administrator 管理者情報 * @param authorityList 権限情報が入ったリスト */ public LoginUser(com.example.domain.User user, Collection<GrantedAuthority> authorityList) { super(user.getEmail(), user.getPassword(), authorityList); this.user = user; } public com.example.domain.User getUser() { return user; } }5.UserRepositoryの作成
メールアドレスからユーザーを検索するためのリポジトリー。別途userのドメインクラスを作成する必要があるが今回は省略。
※RowMapperのカラムとgetter,setterはプロジェクトに応じて変える必要ありUserRepository.javapublic class UserRepository { @Autowired private NamedParameterJdbcTemplate template; private static final RowMapper<User> USER_ROW_MAPPER = (rs, i) -> { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setEmail(rs.getString("email")); user.setPassword(rs.getString("password")); user.setZipcode(rs.getString("zipcode")); user.setAddress(rs.getString("address")); user.setTelephone(rs.getString("telephone")); return user; }; /** * メールアドレスからユーザー情報を1件取得 * @param email * @return List<User> */ public User findByMailAddress(String email) { String sql = "select id,name,email,password,zipcode,address,telephone from users where email = :email"; SqlParameterSource param = new MapSqlParameterSource().addValue("email", email); List<User> userList = template.query(sql, param, USER_ROW_MAPPER); if(userList.size() == 0) { return null; } return userList.get(0); }注1・・・SpringSecurityを実装した状態でログインする時には、データベースに入っているパスワード情報がハッシュ化されている必要がある。ユーザー情報登録の際のパスワードハッシュ化については別記事参照。
- 投稿日:2019-10-08T08:21:50+09:00
GradleでSpring Bootのプロジェクトを環境別にビルドする
Gradleで環境別にビルドを実行する方法はいくつかありますが、
今回はSpring Bootのapplication.propertiesの設定を書き換えてからwarファイルを出力する方法を紹介します。使用する開発ツールはSTSで、Gradle Buildshipが動くことが前提となっています。
最終的には赤枠で囲った部分がGradle Tasksに追加され、
実行することでそれぞれの環境用のwarファイルを出力できるようになります。
準備
Spring Initializrでプロジェクトの雛形を作成します。
ProjectでGradle Projectを選択し、Packagingを選んでGenerateボタンを押してダウンロードします。
今回はwarを出力する想定で動かします。ダウンロードが終わったらzipファイルを解凍して
STSにGradleプロジェクトとしてインポートしましょう。ディレクトリ構成
demo │ build.gradle // ここにビルドスクリプトを追加する │ application_template.properties // 新しく作成する │ ├─.gradle ├─.settings ├─bin ├─gradle └─src ├─main │ ├─java │ │ └─com │ │ └─example │ │ └─demo │ │ DemoApplication.java // 修正する │ │ │ └─resources │ application-dev.properties // 新しく作成する │ application-product.properties // 新しく作成する │ application.properties // 修正する │ └─test設定ファイル
Spring Bootではapplication-環境名.propertiesというファイルを用意し、
application.propertiesに環境名を設定することで環境別の設定を適用することができます。
ということで、resources配下に以下のような3つのファイルを用意してください。application.propertiesspring.profiles.active=devapplication-dev.propertiessample_data=Development Environmentapplication-product.propertiessample_data=Production Environmentサンプルアプリケーション
今回は環境別の設定ファイルの内容を表示するというめっちゃシンプルなアプリを動かします。
以下のようにApplication.javaを書き換えましょう。DemoApplication.javapackage com.example.demo; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class DemoApplication { @Value("${sample_data}") private String data; // この値が設定ファイルのsample_dataの値から読み込まれる public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args); DemoApplication demo = ctx.getBean(DemoApplication.class); demo.run(); } private void run() { System.out.println(data); } }実行するとapplication.propertiesで設定されたファイルが読み込まれ、その環境に応じた値が表示されるはずです。
実行結果(Springのログは略)Development Environmentビルドスクリプトを書く
今回はSTSのGradle Tasksビューから環境別のタスクを実行しwarを出力します。
まずはapplication.propertiesをビルド対象の環境が設定されたものに置き換える処理を実行するようにします。
以下のようなapplication.propertiesの雛形のファイルを作成しましょう。application_template.propertiesspring.profiles.active=${buildEnv}次にapplication.propertiesを雛形ファイルの${buildEnv}の部分を置き換えたファイルで上書きするタスクを記述します。
build.gradleに追記しましょう。
また、今回はwarコマンドを使用しますので、warコマンドの設定を記述するのも忘れないようにしておきましょう。build.gradleplugins { // (中略) } // warコマンドの設定を追加 war { enabled = true archiveName 'sample.war' } // (中略) // 各ファイル名 def templateFile = 'application_template.properties' def configFile = 'application.properties' // 対象の環境名(後に値を代入する) def targetEnv = '' task replaceConfig { description 'Replace application.properties environment.' doLast { println ' start replacing :' + targetEnv // コピー実行部分 copy { // from: コピーするのファイルのあるディレクトリを指定する from '.' // include: 今回はコピーするファイル名を指定している include templateFile // into: コピー先のディレクトリ into 'src/main/resources' // application.propertiesにリネームする rename(templateFile, configFile) // ${buildEnv}を置き換える expand([ buildEnv: targetEnv ]) println " replacing finished" } } // 置き換えタスクが終了したらwarコマンドでwar出力する(jarファイルならjarにする) finalizedBy { war } }これでコピー処理実行後にwarタスクが実行されるようになりました。
ですが、これだけではまだtargetEnvの値が空文字なので設定する部分を記述します。
更に追記します。build.gradle// (中略) def templateFile = 'application_template.properties' def configFile = 'application.properties' def targetEnv = '' task replaceConfig { // (中略) } task warProduct { // groupを設定することで Gradle Tasksビューのbuildの中に表示される group = 'build' description 'Create war file for product environment.' doLast { // targetEnvを設定する targetEnv = 'product' } // targetEnv設定後、replaceConfigタスクを実行する finalizedBy { replaceConfig } } // devもProductと同様 task warDev { group = 'build' description 'Create war file for dev environment.' doLast { targetEnv = 'dev' } finalizedBy { replaceConfig } }それぞれのタスク内でtargetEnvを設定後、replaceConfigタスクを実行するように記述しました。
これで動作するはずです。
以下はノーカット版です。build.gradleplugins { id 'org.springframework.boot' version '2.1.9.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java' id 'war' } war { enabled = true archiveName 'sample.war' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' } // 各ファイル名 def templateFile = 'application_template.properties' def configFile = 'application.properties' // 対象の環境名(後に値を代入する) def targetEnv = '' task replaceConfig { description 'Replace application.properties environment.' doLast { println ' start replacing :' + targetEnv // コピー実行部分 copy { // from: コピーするのファイルのあるディレクトリを指定する from '.' // include: 今回はコピーするファイル名を指定している include templateFile // コピー先のディレクトリ into 'src/main/resources' // application.propertiesにリネームする rename(templateFile, configFile) // application_template内の${buildEnv}を置き換える expand([ buildEnv: targetEnv ]) println " replacing finished" } } // 置き換えタスクが終了したらwarコマンドでwar出力する finalizedBy { war } } task warProduct { // groupを設定することで Gradle Tasksビューのbuildの中に表示される group = 'build' description 'Create war file for product environment.' doLast { // targetEnvを設定する targetEnv = 'product' } // targetEnv設定後、replaceConfigタスクを実行する finalizedBy { replaceConfig } } // devもProductと同様 task warDev { group = 'build' description 'Create war file for dev environment.' doLast { targetEnv = 'dev' } finalizedBy { replaceConfig } }これで完成です。
Gradle TasksからwarDev、もしくはwarProductを実行するとbuild/lib内にwarファイルが作成されます。
warを解凍して、その中のapplication.propertiesを確認すると、各環境の設定になっているはずです。
- 投稿日:2019-10-08T06:55:47+09:00
Spring Bootでテストコードを書いてみる
概要
Spring BootでMVC関連の処理やログイン関連の処理のテストコードについて、あれこれ試行錯誤しながら書いたので、備忘録としてまとめておきます。
※ログイン関連の処理については別記事で頑張って作成中です...ƪ(˘⌣˘)ʃもっといい書き方等ご存知でしたらやさしく教えて頂けるとうれしいです(╹◡╹)
ソースコードはGitHubで公開しています。
読まなくても何とかなる前置き(クリックで開閉します)
昨今、テストコードを書いた方がいい、という話がしばしば耳に入ってきていました。
それなら書いてみるか、と思って調べてみると、大体四則演算レベルのテストで止まってしまっており、実際のアプリでどう書いたらいいの...!?
となってしまい、いまいちテストコードを書くのに踏み切れていませんでした。
このままではマズいなと思ったので、お盆休みを生贄に捧げ、Spring Bootでエラーに苦しめられながら、試行錯誤しつつテストコードを書いてみました。
まだおぼつかない点も多々ありますが、とりあえずある程度「書き方のパターン」のようなものは確立できてきたので、備忘録も兼ねてまとめてみようかと思います。
Spring Bootのテストコードに関する記事は日本語・英語問わず体系的にまとまっているものがあまり無いので、色々と手探りの部分もありますが、少しでも理解の一助となれば幸いです('ω')対象読者・ゴール
本記事ではSpring Bootでのテストコードの書き方にフォーカスしていくので、以下を満たしていると、つらくないかと思います。
- MVCの雰囲気は理解していて、Spring Bootで簡単なTODOリストレベルのアプリは作れる
- JUnitは何となく触ったことがあって、四則演算レベルならとりあえずテストコードは書ける
上記の前提があった上で、本記事を読むことで、(きっと)以下のことが可能となります。
- Spring Bootを利用した簡単なデータベース操作のユニットテスト
- Spring Bootを利用した簡単なCRUDアプリのユニットテスト
- Spring Bootを利用したログインが必要なアプリの基本的な処理のユニットテスト(別記事)
また、今回は画面の表示やJS部分等のテストについては対象外としています。将来的にはその辺りもカバーしていくつもりですが、いきなり全てをテストしようとすると複雑になり過ぎてしまうので、まずは小さなところから始めていきます。ステップアップ大事。
補足: テストはどこまで自動でやるか(クリックで開閉します)
テストコードを書くことの重要性については、頻繁に取りあげられていますが、手動テストを自動テストに置き換えるのは多くの経験・コストを必要とします。
業務アプリケーションであれば、色々とあれやこれやがあって、自動テストを中々取り入れられないということがあったりします。
しかし、個人開発では何の制約も無いからといって、いきなり全てを自動化だ!!と躍起になると大体心が折れます(私は折れましたƪ(˘⌣˘)ʃ)。
そこで、まずは「自動テストで何ができると助かるか」という部分にフォーカスし、できる範囲で自動テストできる部分を少しずつ増やしていくのが、つらくないやり方かと思います。
テストコードを書くのはフレームワーク・言語の十分な理解が必要となってくるので、自分の技術力の成長に応じて、長く付き合っていくのがよいかと思います。
さて、話を今回のテストコードの範囲に戻しましょう。
Spring Bootでアプリをごりごり書いていると、大体以下のような部分でバグが入り込むのを(個人的に)多く経験してきました。
・ Viewへ渡すModelへ想定した値が渡されていない
・ Daoレイヤーの処理の実行結果をDBで見てみたら想定通りになっていない
・ ユーザの権限に応じた処理が思うように動作していない
上記は今回のカバー範囲といい感じに重なっています。
まずはバグがたくさん発生し、しんどい思いをしている部分のデバッグを効率良くできるようにすることで、開発の楽しさを上げていくことを目指していけたら、と思います。
上記のカバー範囲を簡単に図で表すと、以下のようになります。
リクエストが投げられてからViewさんに表示が依頼される辺りまでを触れていきます。そのため、今回はViewさんは基本的にはおやすみです。
環境
詳細はGitHubのpom.xmlを見て頂くとよいかと思います。
pom.xml(抜粋)<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.dbunit/dbunit --> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.5.1</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/com.github.springtestdbunit/spring-test-dbunit --> <dependency> <groupId>com.github.springtestdbunit</groupId> <artifactId>spring-test-dbunit</artifactId> <version>1.3.0</version> <scope>test</scope> </dependency>テスト範囲
さて、ここから実際にテストコードの書き方について見ていきます。
しかし、Spring Bootのテストコードはいくつかの知識を組み合わせて記述していくため、一気に全てをカバーしようとすると、難易度が跳ね上がってしまいます。そのため、以下の4つのステップに分けていきたいと思います。
- Level1. HelloWorldをテストする
- Level2. データベース操作をテストする
- Level3. POSTリクエストをテストする
- (Level4. ログインが必要なアプリをテストする)
※レベル4に関しては、本記事で取りあげるテストコードの知識を総動員する必要があり、ボリュームが大きくなってしまうため、別記事に分けます。
本記事で扱うレベル1〜3のカバー範囲については、大雑把に図で表すと、以下のようになります。
それでは、早速、HelloWorldから見ていきましょう。
Level1. Hello Worldをテストする
お馴染みのハローワールドさんです。
以下の「/hello/init」へリクエストが投げられると、view名として「hello」が返される、というようなよくある処理について見ていきます。HelloController.java@Controller @RequestMapping("/hello") public class HelloController { @RequestMapping("/init") private String init() { return "hello"; } }やっていることは単純なのですが、テストコードを書く際にはいくつか新しい知識が必要となるので、Spring Bootのテストコードへ入門する上では、とても大事な部分となります。
まずは、コード自体はあまり長くないので、全体像を掴むためにも、以下へ実際のテストコードを記述します。
HelloControllerTest.java@AutoConfigureMockMvc @SpringBootTest(classes = DbMvcTestApplication.class) public class HelloControllerTest { //mockMvc TomcatサーバへデプロイすることなくHttpリクエスト・レスポンスを扱うためのMockオブジェクト @Autowired private MockMvc mockMvc; // getリクエストでviewを指定し、httpステータスでリクエストの成否を判定 @Test void init処理が走って200が返る() throws Exception { // andDo(print())でリクエスト・レスポンスを表示 this.mockMvc.perform(get("/hello/init")).andDo(print()) .andExpect(status().isOk()); }いきなり普段アプリを書いている上では、見慣れないものがたくさん出てきてしまいました。
このテストコードの処理は、大きく分けて「2つ」のブロックに分かれているので、それぞれについて詳しく見ていきましょう。クラスのアノテーション
クラスへ付与されているアノテーションは、テストコードの全体像を知る上で重要な情報を持っています。
普段使わないものばかりですが、しっかりと概要を掴んでおくと、テストコードとの距離がぐっと縮まります。AutoConfigureMockMvcアノテーション
MockMvc
というものを利用するためのアノテーションです。では、MockMvcとは一体何者なのでしょうか。これは、物理的な「サーバ」と作成した「Webアプリケーション」を切り離す為のものです。
いまいちピンと来ないので、切り離すことで何が嬉しいのか、という観点で見てみましょう。
- なんらかのエラーが発生した場合、原因はWebアプリケーションに「限定」される
- テストの度にサーバが起動するまで待つ必要がないので、テストケースの実行時間が短縮される
補足: モックを使う必要性(クリックで開閉します)
モックを使うメリットで実感しやすいのは、テストコードの実行時間の短縮なのですが、影響範囲の限定というのも大事な要素です。
例えば、Dao層にアクセスするサービス層の処理をテストする際、簡単な処理であれば、モックを使わずともテストはさほど苦労することなく実現できるかと思います。
しかし、コードが複雑になり、更に作る人も別々というようになった場合、バグが発生すると原因が「サービス層」にあるのか、「Dao層」にあるのか、切り分けの為の調査が必要になってしまいます。
ここで、Dao層を「適切に」モック化しておき、サービス層のテストを先に完了させてしまえば、実クラスに差し替えた際にバグが発生した場合、Dao層に原因を限定させることが可能となります。
実際のところ、個人開発ではモックを使わずとも、一から百まで全て自分で実装しないといけないので、「実行時間」を抑える以外の目的でモックを使う機会はあまり無いかもしれません。しかし、テストコードに限らず、影響範囲を限定させる、という考え方は重要になってきますし、チーム開発では全てを自分一人で作るわけではないので、モックの考え方・扱い方については、概要レベルでも知っておくとよいかと思います。
ただ、私もモックが完全に使いこなせているとは、とても言えないような状態なので、もっといい考え方等ご存知でしたら、教えて頂けるととてもうれしいです。以下、参考資料です。
Mockオブジェクトとは
Mockのメリット
個人開発をする上では、サーバ部分をモック化してテストコードの実行時間を抑え、チーム開発では、自身が携わっていない部分をモック化し、影響範囲を限定した上でテストコードを書く、という形で進めるのが良いかと思います。
※メール送信処理といった外部サービスと連携が必要な処理については、個人開発でもモックを使った方がよいかと思います。
以下、参考資料です。
SpringBootTestアノテーション
いかにも重要そうなアノテーションです。実際すごく重要なアノテーションです。
Spring Bootのユニットテストでは、必ずと言っていい程出てくるアノテーションで、たくさんの機能を備えているので、段階的に機能を見ていく形にしたいと思います。ここで重要となるのは、以下の2つの機能です。
- ExtendWithアノテーションの隠蔽
- ApplicationContextの指定
まずは、
ExtendWithアノテーション
についてです。Spring Bootのテストについて解説したものではよく、RunWithアノテーションが用いられていましたが、RunWithアノテーション
はJunit4用のアノテーションで、ExtendWithアノテーションはJunit5用のアノテーションとなっています。用途としては、テストの事前処理や事後処理などを汎用的に実装するために用います。そして、valueプロパティである、汎用処理の実装が書かれた「Extension」クラスには、SpringExtensionが渡されます。
発展的な話になるので、今回は割愛しますが、このExtensionクラスは、例えば、DIコンテナの役割を持つApplicationContextのインスタンス化といった重要な部分を担っています。
詳細は公式をば。さて、長々と書きましたが、Spring BootでDIコンテナを利用するのは最早当たり前となっており、毎回ExtendWithアノテーションを記述するのは手間となってしまいます。
ドキュメントを見て頂くと分かる通り、使うのが当たり前になっているなら内包しちゃえば良いじゃない、ということでSpringBootTestアノテーションだけでOK、という形となりました。このように複数の機能を内包したアノテーションを自前でも作成することで、テストコードの前提の記述を簡略化することもできますが、今回は分かりやすさを重視する、ということで、統合用のアノテーションは割愛しております。
続いて、ApplicationContextの設定について見ていきます。
度々言葉として出てきているApplicationContextですが、ここではざっくりと「DIコンテナ」ぐらいに思って頂いても問題はないかと思います。
※DIコンテナに関する説明はこの辺りが分かりやすくておすすめです。
Spring Bootのユニットテストでは、DIコンテナが「どのクラスを登録するのか」明示しておく必要があります。
といっても、特別なことが必要というわけではなく、SpringBootApplicationアノテーション
が付与されたクラスをclassesプロパティへ設定することで、Configurationアノテーションが付与された設定クラス、ComponentScanの対象となるクラスを自動的にDIコンテナへ登録してくれます。
参考単純にメインクラスを渡せば全て解決、というわけにはいかないのですが、その辺りはHTTPリクエスト・レスポンスを必要としないDaoレイヤーでのテストの辺りで触れたいと思います。
補足: classesプロパティへ渡すべきもの(クリックで開閉します)
ドキュメントにある通り、「classes」プロパティに設定されるクラスは、「Configuration」の役割を持ったもので、DIコンテナを実現するために利用されます。先ほどの説明の中では、SpringBootApplicationアノテーションが付与されたものがDIコンテナのための「設定」を実現していました。
しかし、今回のHelloControllerはそもそも(見えている範囲では)他のクラスへは依存していないので、「classes」プロパティへ「HelloController」を渡しても動作はします。
ですが、「classes」プロパティは本来、設定クラスとしての役割を持ったクラスが渡されることが想定されているので、SpringBootApplicationアノテーションが付与されたクラス、あるいはテストに必要な設定を定義したコンフィグクラスを渡すのがよいでしょう。
何か機能を利用する際には、ドキュメントに記載されているパラメータが想定しているものは何か、ということだけでも最低限目を通しておくと、予期せぬ動作でハマって途方に暮れる、ということも少なくなるかと思います。(自戒)
※実際、HelloControllerのみをWebApplicationContextとして定義してしまうと、MockMvcのレスポンスのBody部分が空となってしまいます。
(中身までは見られていませんが、ViewResolverやViewがインスタンス化されていないことが原因だと思われます。)
設定だけで説明が長くなってしまいましたが、一度理解してしまえば他のテストコードを書く際にも活用できる重要な部分なので、しっかり腰を据えて学習してみるとよいかもしれません。
(私はDIとか全く理解していないことを理解したので、Springについて復習するいい機会になりました)テストコード
ようやく実際のテストコードへたどり着けました。
今までの細かな設定よりはシンプルで、そして楽しい部分です。
間隔があいてしまったので、テストコードに関する部分をもう一度見てみましょう。HelloControllerTest.java(抜粋)// getリクエストでviewを指定し、httpステータスでリクエストの成否を判定 @Test void init処理が走って200が返る() throws Exception { // andDo(print())でリクエスト・レスポンスを表示 this.mockMvc.perform(get("/hello/init")).andDo(print()) .andExpect(status().isOk()); }上記のテストコードは、「リクエストの実行」、「レスポンスの検証」の二つの処理を行なっています。
いずれの処理も、「MockMvc」のインスタンスをベースに行なっており、基本的には上記のように一つの文で記述します。
一つの文にまとめることで、テストが何をやっているのか、英語の文章のような形で紐解くことが可能となります。
実際にどのように書かれているのか見てみましょう。
- 実行しなさい(perform)
- /hello/initへのGETリクエストを(get)
- 結果を表示しなさい(andDo(print))
- (結果を)期待します(andExpect)
- HTTPステータスコードが200であることを(status().isOK())
上記をまとめると、以下のような英文になるでしょう。(ライティングはあまり得意ではないので雰囲気で感じ取ってくださいƪ(˘⌣˘)ʃ)
Perform get request to [/hello/init] and print the result, and I expect that status is 200 OK.プログラミング言語は英語で書かれているので、当然、英語ネイティブの方であれば、普段慣れている英語の文章のような構造でテストコードを読み書きすることができる、というメリットがあります。
(英語から逃げる手段は無さそうなので、せめてリーディング・リスニングだけでもそれなりにかじっておくと、つらくないかもしれません)少し話は逸れましたが、テスト方法・期待結果を見やすい形で記述することで、システムの仕様や、実際のソースコードを理解する上で重要な情報がテストコードから容易に手に入るようになります。
裏を返せば、仕様を満たさないようなテストコードはバグを見逃してしまう上、仕様の理解にも繋がらないので、ダメなコードとなってしまいます。こういう簡単なコードの段階から、
実装した処理では何を機能として実現できていればよいか
ということを常に意識しながらテストコードを書く習慣をつけると良いのかもしれません。色々と余談を挟みましたが、HelloWorldをテストコードで検証した際のリクエスト・レスポンスは、以下のようになります。
上記で記した内容が満たされていることが確認できているかと思います。
これでようやくHelloWorldが正しく動いていることが確認できました。やったぜ。
Modelをテストする
HelloWorldのテストコードだけでは検証できることが限られてしまっているので、レベル1ではもう一つ、実用的なものとして、Modelの検証について見ていきたいと思います。
ここでのModelは「Viewから参照されるJavaオブジェクト」を指しており、
Model.addAttribute
等でよく書かれるものです。
Modelへいい感じに値を詰めたと思ったら実は入っていませんでした、ということは非常によくあることなので、サーバを起動して画面へかちかちアクセスして...とすることなく、一発で動いているかどうかが検証できたら、とても便利です。以下では、実際にModelの中身をテストコードで検証する方法について見ていきます。
これまで触れてきたものと比べればサクッと理解できる内容なので、しっかり使い方をマスターして頂ければと思います。
検証対象コード
まずは、テスト対象となるコードを見ていきます。
といっても、先ほどの HelloControllerを少し拡張した程度のもので、全体を通してさほど難しいことはしていないので、ここで一気に全体を載せてしまいます。HelloController.java@Controller @RequestMapping("/hello") public class HelloController { @RequestMapping("/init") private String init(Model model) { // ユーザリスト まずは手動で生成 List<User> userList = new ArrayList<User>(); User user = new User(); user.setUserId(0L); user.setUserName("test0"); User user2 = new User(); user2.setUserId(1L); user2.setUserName("test1"); userList.add(user); userList.add(user2); // フォームにユーザのリストを設定し、モデルへ追加することでモデルへ正常に追加されたか検証するための土台を整える DbForm form = new DbForm(); form.setUserList(userList); model.addAttribute("message", "hello!");// 1 model.addAttribute("user", user);// 2 model.addAttribute("dbForm", form);// 3 return "hello"; } }以下では、
model.addAttribute
について、それぞれのパターンを追っていきます。① ModelにStringが格納されたか
最初は単純な例から見ていきましょう。
model.addAttribute("message", "hello!");
では、単純にモデルはkeyとして「message」を、valueとして「hello」を格納しています。これを検証するためのテストコードは、以下のようになります。
HelloControllerTest.java(抜粋)@Test void init処理でモデルのメッセージにhelloが渡される() throws Exception { this.mockMvc.perform(get("/hello/init")) .andExpect(model().attribute("message", "hello!")); }テストコードもすごくシンプルで、
andExpect(model().attribute("message", "hello!"))
という部分を見て頂くと分かる通り、実際のModelへの詰め込みとほぼ同じような形でテストコードも書くことができます。実際に結果を見てみると、Model部分へ正しく値が詰め込まれていることが分かります。
単純なオブジェクトであれば、プロパティが一階層のみとなっているので、シンプルに書くことができます。
しかし、結果の中をよく見て頂くと、何やらvalue値が怪しげな文字列で書かれたものがあります。
これは、オブジェクトのインスタンスそのものを表しています。当然、オブジェクトがnullでないかを検証したいときもありますが、大体の場合、オブジェクトの中の特定のプロパティが想定通りの値になっているか、という部分まで知りたいものです。以下ではこういったネストしたプロパティが詰め込まれたModelの検証について、見ていきます。
②Modelへ詰め込まれたユーザEntityのuserNameプロパティの値は想定通りか
さて、ネストしたオブジェクトについての検証は、少しだけテストコードが複雑になりますが、アノテーションの解釈に比べれば、とてもシンプルなので、一つ一つ探っていきましょう。
ここでは、
model.addAttribute("user", user);
の処理について、「user」インスタンスのプロパティ「userName」が想定通り(ここでは、「test0」という値)になっているか、検証していきます。何はともあれ、実際のテストコードを見てみることから、始めていきます。
HelloControllerTest.java(抜粋)@Test void init処理でモデルへユーザEntityが格納される() throws Exception { this.mockMvc.perform(get("/hello/init")) .andExpect(model() .attribute("user", hasProperty( "userName", is("test0") ) ) ); }いきなり構造がガラッと変わりました。
これは、Spring Bootのテストコードというよりは、「Hamcrest」というテストの妥当性を検証する、いわゆる「Matcher」を扱うフレームワークによる処理となっています。※HamcrestはMatchersのアナグラムを表しています。
オブジェクトのプロパティを検証するためのメソッド
hasProperty
はstaticインポートで使用しているので、正確には、HasPropertyWithValue.hasProperty
という構造となっています。そして、このメソッドは以下のような役割を持ちます。(公式より引用)
Creates a matcher that matches when the examined object has a JavaBean property with the specified name whose value satisfies the specified matcher.
何やら難解な英文が出てきましたが、実際の例を見てみるとしっくり来るかと思います。
assertThat(myBean, hasProperty("foo", equalTo("bar"))
これは、「myBean」というオブジェクトは、「foo」というプロパティを持っており、fooプロパティの値は「bar」である、ということを表しています。まさに今回のテストで検証したいことそのものですね。
今回の例を簡単に書くと、assertThat(user, hasProperty("userName", is("test0"));
というようになります。
hasPropertyの戻り値はMatcher型に属するので、プロパティをネストして書くこともできます。ネストしたプロパティに対する検証はどうしてもコードが長くなり、括弧の対応関係が分かりづらくなってしまうので、上記の例のようにインデントを工夫する等読みやすくするための何らかの配慮は必要になるかと思います。
何はともあれ、これでModelが複雑になっても対応できるようになりました。
最後のModelの例として、今度はListオブジェクトに関するテストコードを見ていきたいと思います。
補足: Matcherとは
割と当たり前のように出てきたMatcherですが、JUnitに触れていないとあまり馴染みが無いかと思いますので、補足で簡単に触れておきます。
Matcher自体はインタフェースで、テストでの検証(値が等しいか、等しく無いか、全て条件を満たすかなど...)処理を簡単に、かつ読みやすい形で記述するために作られました。
例えば、よく使われる「assertThat」メソッドはassertThat(1 + 1, is(2));
といった形で記述されます。これは、先ほどのperform処理と同様、英語の読みやすい形でテストコードを記述するためのものです。
そして、assertThatメソッドの第二引数に指定するものこそが「Matcher」を表しているのです。実際にassertThatメソッドの中身を追ってみると、
if (!matcher.matches(actual)) { 一致しなかったときの処理 }
という記述があり、ここでは、第二引数に指定したMatcher型のインスタンスのmatchesメソッドを呼び出しています。
この結果はbooleanとなるので、Mathcerというのは、単に検証の成否を格納するための真偽値というようにざっくり解釈しておくとよいかと思います。
ネストしたプロパティであっても、結局は最下層の特定のプロパティ値が想定したものと等しいかを検証しているに過ぎないので、あまり身構えず、シンプルに考えると案外スッキリ理解できるかもしれないです。
③Modelへ詰め込まれたリストは想定通りのプロパティを保持しているか
最後のModelのパターンとして、ネストしていて、かつ、リスト構造を持つものについて見ていきましょう。
model.addAttribute("dbForm", form);
というよくある例として、「Formオブジェクトの中のユーザのリスト」が想定したものとなっているかを検証します。
例のごとく、まずは以下へコードを記載します。HelloControllerTest.java(抜粋)// リスト要素については、hasItemで順番を問わずリストへアクセスし、指定されたプロパティが指定の値となる要素が存在するかを検証。 // 存在する場合のみテストをグリーンとする @Test void init処理でモデルのフォームへユーザリストが格納される() throws Exception { this.mockMvc.perform(get("/hello/init")) .andExpect(model().attribute("dbForm", hasProperty( "userList", hasItem( hasProperty( "userName", is("test1") ) ) ))); }新たに
hasItem
というメソッドが出てきました。
公式さんいわく、リスト形式のオブジェクトに対してのみ使用可能となっています。
そして、ここで利用しているメソッドは、引数として、Matcherを持ちます。つまり、ざっくりまとめると、hasItemメソッドの実行対象となるリストの中に、引数として渡されるMatcherを満たすものが一つでも存在するか、ということを検証しています。
今回の例では、ユーザリストの中の各ユーザ要素について、「userName」プロパティが「test1」であるものが一つでも存在するか
が検証したいこと、となります。例でのリストは要素数が「2」程度の小さなものなので、中身を全て調べることも可能ではありますが、実際のアプリでは数百、数千と要素が詰まったリストが渡されることがよくあります。これを全て検証するのはいくらコード化できるとはいえ、しんどそうです。
こういう場合、先頭、中間、末尾辺りの要素について、仕様を満たしているか検証できればある程度信頼性は担保できそうです。
よって、リストの要素を丸ごと全て検証するのではなく、代表要素として、一部さえ検証できればよいので、hasItemメソッドでいくつかテストケースを設けてあげるのが、よいかと思います。※もちろん、業務要件が厳密で、一つの誤りが致命的な損失に繋がる場合は、この限りではないです。
さてさて、色々と補足説明を交えながらとなったので、だいぶ長くなってしまいましたが、これでレベル1のテストコードは検証完了です。
レベル1をこなすことによって、以下のことが可能となりました。
- 簡単なGETリクエストであれば、リクエスト・レスポンスが正常であるか検証できる
- Viewへ渡されるModelオブジェクトへ、想定した値が詰め込まれているか検証できる
- Spring Bootでのユニットテストで最低限必要なアノテーションの概要が理解できる
続いて、レベル2では、アプリケーションの要である、データベースについての検証を見ていきたいと思います。
Level2. データベース操作をテストする
Spring Bootでのユニットテストでは、「DbUnit」というものを利用し、データベースの検証を行います。
こう書くと、Spring Bootだけでも手一杯なのに、更にまた勉強することが増えるのか...となってしまいますが、DbUnitは簡単な使い方を覚える程度で十分かと思います。
重要なのは、DbUnitとSpring Bootを組み合わせることで、「どのような作業が楽になるのか」を意識することです。いきなりDbUnitについてごりごり書いていくと、イメージがわきづらいかと思うので、まずは、データベース操作が手動テストからどのように自動テストへ置き換わるか、流れを追ってみましょう。
手動テスト
最初に、手動でデータベース操作をテストすることを考えてみましょう。
大体、以下のような流れでテストが進められるかと思います。
- アプリ側でデータベースからSELECTでレコードを取得する処理を実行
取得結果が想定したものとなっているか取得されたレコードを見ながら検証
アプリ側でデータベースのレコードをUPDATE、DELETEする処理を実行
処理の適用前後でデータベースのレコードを比較し、想定通りの結果となっているか検証
手動テストの是非についてはここでは置いておいて、ここでは、「テストの再現性」について着目してみます。
チームで開発を行っていればSELECTの結果は刻一刻と変化しますし、UPDATE、DELETE処理も、同じ条件を揃えようと思ったら事前準備がある程度必要となってしまいます。
実行したテストに再現性が無ければ、リファクタリング、リグレッションテスト等で繰り返しテストを行った際、本当にデグレードが起こっていないのか特定が困難になってしまいます。それでは、手動テストについて、もう少し工夫してみましょう。
テスト状態の手動構築
再現性を担保するため、上記の手動テストへ以下のような工程を取り入れてみました。
- CSV(もしくはXML)ファイル等でテスト開始時のレコード状態をデータベースとは別で作成
- データベースの既存レコードはバックアップ等で退避し、テスト用ファイルによって差し替え
- UPDATE、DELETEの検証用のファイルを別途用意し、処理の適用結果が想定と等しくなるか、レコードとファイルを比較することで検証
- データベースの既存レコードを復元させてテスト完了
これでテストの再現性は担保できました。これなら安心してテストできる...!!ということには当然なってくれません。
小規模なアプリであれば実現できないことは無いかもしれませんが、ちょっとした検証の度にデータベースを丸ごとバックアップして、丸ごと削除して、丸ごと復元して...とやっていると、テストに膨大な時間が掛かってしまいます。せっかく再現可能なテスト状態を作っても、テスト自体の実行や、結果が返ってくるまでに多大な時間が掛かってしまうと、適切なタイミングでテストが実行されなくなってしまいます。
ちょっとデバッグしたいと思っただけなのに、結果が返ってくるまでに数十分、下手したら数時間待たされているようでは結局後回しにされ、手動でカチカチテストする形に戻ってきてしまいます。
ここまで、データベース操作のテストを再現性を担保しながら行うのは、ややハードルが高いように感じられたかもしれません。
しかし、Spring BootとDbUnitを組み合わせることで、多少の準備は必要になりますが、上記のようなテストをボタン一発で実行可能となります。それでは、レベル2の本題として、Spring Boot, DbUnitを利用したテストコードについて、見ていきましょう。
検証対象コード
まずは、王道のSELECTで結果を取得する処理について見ていきます。
以下へ検証対象となるDao層のコードを記載します。UserDao.java(抜粋)/** * DBから全てのユーザレコードを取得する * 今回はテストのため、処理を簡単なものとした。 * @return ユーザエンティティのリスト */ public List<User> findAllUser() { QueryBuilder query = new QueryBuilder(); query.append("select user_id, user_name from tm_user"); return findResultList(query.createQuery(User.class, getEm())); }色々と処理が書かれていますが、注目すべきは、以下の2点となります。
- ユーザテーブルから全てのユーザをSELECT
- 結果をユーザエンティティのリストとして返却
データベースからレコードをSELECTする処理はこれからも繰り返し出てくるので、まずは最初のステップとして、
データベースのレコード数 = リストのサイズ
を検証します。検証のため、早速DbUnitを利用していくのですが、実際のテストコードを書く前に、いくつか前準備が必要となります。
前準備は少しやることが多いですが、一度マスターしてしまえば、以降のデータベース操作のテストではルーチン化できるので、ゆっくりと見ていきたいと思います。CSVでデータを管理したい
最初に、データベースのレコードをファイルで管理するための土台を作っていきます。
DbUnitの標準機能としては、XMLファイルでレコード・トランザクション設定等を記述していくのですが、今回はCSVでレコードを管理していきます。
理由は色々とありますが、ざっくりまとめると、シンプルに書ける
ということが大きなものとなります。以下ではいくつかのステップを踏んでいきますが、どれもやっていること自体はシンプルなので、DbUnit自体にあまり踏み込まなくてもある程度直感的に理解できるかと思います。
ということで、まずはCSVファイルをテストで利用するためのクラスを作成していきます。
CsvDataSetLoader
CsvDataSetLoader.classpublic class CsvDataSetLoader extends AbstractDataSetLoader{ @Override protected IDataSet createDataSet(Resource resource) throws Exception { return new CsvURLDataSet(resource.getURL()); } }以下では重要な要素について、補足説明を記載します。
- AbstractDataSetLoader
文字通り、何らかのデータセットを読みこむための抽象クラスです。ここでのデータセットは、「テーブルの集合」ということを表しています。この抽象クラスはDataSetLoaderインタフェースの実装クラスとなっているので、作成するクラスは、「DataSetLoader」型となります。
つまり、クラスレベルで作成したクラスを見てみると、「これはデータセットを読みこむ為のクラスです」という情報を記述しているだけのシンプルなものとなっています。
- createDataSet
これまた名前から分かる通り、データセットを作成するためのファクトリメソッドとなっています。
引数として渡されるResource型の「resouce」オブジェクトは、「実ファイル」へアクセスするための情報・振る舞いを持っています。
実際のテストでは、resourceオブジェクトは処理対象のCSVファイルのパスが格納されている、といった形となります。
- CsvURLDataSet
公式で
This class constructs an IDataSet given a base URL containing CSV files
とあるように、上記のresourceオブジェクトをもとにCSVの実ファイルを取得し、データセットオブジェクトへ変換することで、DbUnitが処理できるようにします。いくつか処理は書かれていましたが、このクラスは、クラス名が表す通り、CSV実ファイルを読み取り、データベース操作のテストで利用可能とするためのものとなっています。
一度書いてしまえば、他のアプリでCSVファイルを利用したデータベース操作のテストを行う際にも使いまわせるので、ここでは、各処理が「何を表しているのか」という概要が掴めれば問題は無いかと思います。
CSVを読みこむためのクラスは完成したので、続いては、実際に読みこませる対象のCSVファイルを作ってみましょう。
ファイルのサンプルはGitHubをば。CSVファイル自体の作り方は色々とありますが、個人的には、DBeaverを利用してレコードからCSVをはき出すと、そのまま使えるのでおすすめです。
CSVファイルを作成する上での注意点としては、以下のものがあります。
- ファイル名はテーブル名と合わせる
- 先頭行にカラム名をカンマ区切りで記述
- カラムはテーブルに存在するすべてのものを記載する必要はなく、テストしたいもののみを記載
また、CSVファイルの置き場所にも気をつける必要があります。
(私は変なところに置いてしまってハマりまくりました。)
基本的には、src/test/resources
以下へ配置する形となります。
参考具体的なファイル・フォルダ構成は以下のようになります。
src/test/resources/testData
配下にテーブル名っぽい感じのCSVファイルがあることが分かります。
そして、隣に見慣れないtable-ordering.txt
というテキストファイルが配置されています。
これは、外部キー制約を防ぐためのもので、データベースのテーブルを読み込む順番を指定します。
具体的な書き方は、以下のようになります。table-ordering.txtTableA TableB TableC※外部キー制約を利用していない場合であっても、table-orderingファイルを作成しないと、DataSetExceptionが発生してしまうので、テスト単位でフォルダを分け、各フォルダへ配置しておくのがよいでしょう。
テストコード
さて、ようやく準備が整ったので、テストコードへ入ることができます。
アノテーションがいきなりぶわっと増えますが、ここを乗り越えればテストを書ける範囲がグッと広がるので、頑張りどころです。DBSelectTest.java@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @SpringBootTest(classes = {DaoTestApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE) public class DBSelectTest { @Autowired private UserDao userDao; // DatabaseSetupのvalueにCSVファイルのパスを設定することで、「table-ordering.txt」を参照し、 // 順次テーブルを作成することでテスト用のテーブル群を作成する // このとき、valueのパスは「src/test/recources」配下を起点とし、CSVファイルのファイル名は // テーブルの名前と対応させることとする // // また、@Transactionalアノテーションを付与することで、テストが終わるとトランザクションをロールバックすることで // 環境を汚すことなくテストができる @Test @DatabaseSetup(value = "/testData/") @Transactional public void contextLoads() throws Exception { List<User> userList = userDao.findAllUser(); // Daoで正常にテーブルからレコードを取得できたか assertThat(userList.size(), is(2)); } }クラスのアノテーション
まずは全体像を掴むためにも、クラスに付与されているアノテーションから始めていきます。
DbUnitConfiguration
読んで字のごとく、DbUnitのあれこれの設定を行うためのアノテーションです。
「dataSetLoader」プロパティへ上記でつくったCsvDataSetLoader
を指定することで、CSVファイルの読み込みが可能となります。
設定は他にも色々とあるようですが、現段階ではCSVを読み込ませるために使う、という認識で問題はないかと思います。TestExecutionListeners
今回のテストコードで最も難しそうな部分です。このアノテーションは、概要をおさえて、何をプロパティに渡すべきかが整理できれば十分だと思います。
概要としては、TestExecutionListener
という、テストコードの実行の前後に行われる処理を定義したもののうち、必要なものを読み込ませるためのものです。各リスナーの説明は、公式にざっくりとまとめられていますが、ここでもよく使うものについて、簡単に記載しておきます。
- DependencyInjectionTestExecutionListener
DIをテストコードでも利用する場合に指定します。指定することで、テスト対象クラスをAutowired等でDIコンテナからインジェクションすることが可能となります。
- TransactionalTestExecutionListener
DB操作の際のトランザクションを設定する際に指定します。DB操作後は、DBを元の状態に戻しておくのが基本なので、DBを扱うテストでは基本的には必須となります。
- DbUnitTestExecutionListener
テストの前後でのデータベースの状態を後述のアノテーションで設定する際に指定します。
名前の通り、DbUnitを利用する際には、基本的には追加しておきます。
補足: DirtiesContextとリスナーのデフォルト設定
ここでは紹介しませんでしたが、DirtiesContextTestExecutionListener
というリスナーもよく見かけるかと思います。
これは、DirtiesContextのハンドラの役割を持っています。DirtiesContextは、テストコードの実行に利用するコンテキストのキャッシュを無効化するためのものです。
DirtiesContextを有効にすると、テスト単位でコンテキストがキャッシュされないため、テストコードの実行に時間が掛かってしまいます。テストコードがコンテキストに変更を加えてしまい、後続のテストへ影響が出る場合に使うもののようですが、基本レベルではまず使うことはなさそうなので、気にしなくてもよさそうです。
当然、使わない、という選択肢をとる場合、「なぜ使わないか」は明確にしておく必要があるので、概念ぐらいはおさえておくとよいかと思います。参考
続いて、話は変わりまして、リスナーのデフォルト設定についての補足です。
公式をぱっと見た感じだと、デフォルトでいくつかが有効になっていて、追加したいものを渡せばいいんだなー、というように思っていました。
しかし、実際は有効にはなっておらず、なんだこれ...と思って公式さんを読みこんでみると、TestExecutionListenersアノテーション
で明示していない場合のみデフォルト設定が有効になるようです。
ここでの例は、DbUnit用のリスナーを有効にするため、明記しましたが、この場合はデフォルト設定に書かれているものであっても、必要なものを渡さなければ動作しなくなってしまいます。
参考
SpringBootTest
さて、二度目の登場です。ここでは、新たに
webEnvironment
プロパティが渡されています。
デフォルト値には「MOCK」が設定されていおり、mock servlet environment
なるものを作成しているようです。詳細は公式にもあまり書かれていませんでしたが、コンソール出力を見る限り、MockMvcで利用するテスト用のDispatcherServletを生成する処理のことを表しているようです。
※DispatcherServletの参考資料Dao層単体では、サーバとリクエスト・レスポンスのやりとりを行う必要がないので、プロパティ値として、「NONE」を設定します。
こうすることでMockMvc用のコンテキストを生成する処理がカットされ、わずかにですが、テストが早く終わってくれます。そして、順序は逆になってしまいましたが、classesプロパティへ渡されるものも変化しています。
といっても、大きく処理が変わったというわけでもなく、コンポーネントスキャンの対象を狭めているだけです。
実際のコードを見て頂くとピンとくるかと思います。DaoTestApplication.java@EntityScan("app.db.entity") @SpringBootApplication(scanBasePackages = "app.db.dao") public class DaoTestApplication { public static void main(String[] args) { SpringApplication.run(DaoTestApplication.class, args); } }読み込む対象を「Dao層」、「Daoが扱うEntity」に限定しています。
アプリの規模が大きくなってくると、パッケージを読み込む時間も長くなり、地味に時間が掛かるようになってしまいます。データベース操作関連の処理は頻繁に変更が加えられ、かつテストコードもばりばり走らせたいものなので、少しでも時間が短くなるよう、読み込み範囲を最小限にしています。このぐらい工夫をしてあげると、大体ストレッチがてら首を一回まわすぐらいでテストが完了してくれます。
更に速度を追求する場合は、Configクラスをごりごり書いてEntityManager周りをあれこれする必要がありますが、それほど致命的にテストが遅いというわけでもないので、そこまでは踏み込まないでおきます。上記の設定はサクッとできますし、それなりに効果も得られるので、基本段階では、このぐらいで十分かと思います。
メソッドのアノテーション
またまたアノテーションの話になってしまいますが、クラスレベルのアノテーションよりはシンプルなので、すっと頭に入ってくれるかと思います。
DatabaseSetup
これは、データベースの「初期状態」を定義するためのアノテーションです。
valueプロパティにCSVファイルが配置されているディレクトリを指定してあげると、CSVファイルをもとにデータベースのテーブルに値を詰めこんでくれます。
また、ディレクトリを切り替えることで、異なる状態を作成することもできます。これにより、手動でテーブルへINSERTしなくても、テストを開始したいときのテーブルの状態をいつでも再現できるようになります。ありがてえ。
Transactional
実際のアプリ開発でもよく使われるお馴染みのものです。通常、このアノテーションが付与されたメソッドは、正常に動作すればコミットし、想定しない動作をすればロールバックする、というように動作します。
ですが、テストコードの場合、テストの度にデータベースを書き換えてしまうと、テストの再現性が失われてしまうので、デフォルトではメソッドの実行のたびにロールバックします。
上記二つのアノテーションを組み合わせることによって、手動でデータベースをバックアップして、ファイルからレコードを詰め込んで、最後に戻して...といったことを自動でやってくれるようになります。実際にテストを実行すると、Daoのメソッドでユーザエンティティのリストが取得でき、想定通りの結果(リストのサイズ=レコード数)が得られていることが検証できます。
SELECT処理を検証することで、データベース操作のテストコードの基本はある程度網羅できたので、他の処理についても一気に見ていきたいと思います。
補足: EntityManagerはどこで設定するべきか
テストそのものの話とは少しずれますが、個人的な経験として、EntityManagerがnullになってしまい、テストが動作しない、ということが度々ありました。
DaoクラスのフィールドにEntityManagerを宣言しておいて、PersistenceContext
アノテーションをつけておけばとりあえず動くだろう、程度の認識だったので、中々解決できず、それなりにハマりました。
実際の動きとしては、LocalContainerEntityManagerFactoryBean
というBeanがEntityManagerFactoryを初期化し、DIによってEntityManagerを生成してくれる、というようになっています。この際、DIコンテナが必要となりますが、SpringではApplicationContextがその役割を担ってくれているようです。
上記のように動作するため、テストクラスでEntityManagerをAutowiredやPersistenceContextアノテーションで取得し、テスト対象クラスへ渡すことでも、一応動くことには動きます。
しかし、それでは実際のアプリ上での動作とテストコードでの動作が異なってしまうので、個人的には以下の形で利用するのがよいかと思います。
・ Daoの基底クラスを作成し、基底クラスのフィールドにEntityManagerを設定。(このとき、PersistenceContextアノテーションを付与)
・ 基底クラスではEntityManagerのgetterのみを公開し、派生クラスではgetterのみを意識させるように制限
・ EntityManagerを利用した基本的なメソッド(merge, persistなど)は基底クラス側で定義しておく
EntityManagerのあれこれは情報も少なく、下手にごりごり書こうとするとハマってしまったので、最初のうちはSpringさん側に任せてしまう形にするのがよいかと思います。テストコードで詰まって実装が全然進まなかったりすると精神衛生上よろしくなかったりするので、備忘録がてら書いておきます。参考
※Repositoryを利用する場合はEntityManagerは完全に隠蔽されているので、意識する必要はおそらくほとんど無いかと思います。
Daoの基底クラス
Daoの派生クラス
データベースのCRUD処理をテストする
CRUD処理のうち、SELECTについては検証できたので、残りの更新・作成処理についても見ていきます。大枠はこれまで得た知識で理解できるかと思いますので、以下へ実際のテストコードを記載します。
CRUDDaoTest.java(抜粋)// テストメソッド実行後の状態をデータベースに反映させるための処理 // 通常、更新系の処理はトランザクションがコミットされるタイミングでデータベースと同期化されるが、 // テスト処理ではコミットしないため、明示的に同期化を行う @AfterEach void tearDown() { userDao.getEm().flush(); } /** * create処理で新規レコードが作成されるか検証する * エンティティによってDBが想定通りに書き換えられたかExpectedDatabaseと比較することで検証 */ @Test @DatabaseSetup(value = "/CRUD/setUp/forCreate") @ExpectedDatabase(value = "/CRUD/create/", assertionMode=DatabaseAssertionMode.NON_STRICT) void createメソッドでユーザが新しく生成される() { User user = new User(); user.setUserName("test3"); userDao.saveOrUpdate(user); } /** * update処理で既存レコードがupdateされるか検証する * エンティティによってDBが想定通りに書き換えられたかExpectedDatabaseと比較することで検証 */ @Test @DatabaseSetup(value = "/CRUD/setUp/") @ExpectedDatabase(value = "/CRUD/update/", assertionMode=DatabaseAssertionMode.NON_STRICT) void updateメソッドでユーザ1を書き換えられる() { User user = new User(); user.setUserId(1L); user.setUserName("test1mod"); userDao.saveOrUpdate(user); } /** * delete処理でレコードが削除されるか検証する * 処理前後のDBを用意し、削除後に想定結果となるか比較することで妥当性を検証 */ @Test @DatabaseSetup(value = "/CRUD/setUp/") @ExpectedDatabase(value = "/CRUD/delete/", assertionMode=DatabaseAssertionMode.NON_STRICT) void deleteメソッドでユーザ1を削除できる() { userDao.delete(1); }いくつか新しいものが出てきたので、それぞれについての概要を簡単に触れていきます。また、データベース操作のCURD処理のテストコードについて、いくつか注意点があるので、その辺りも見ていきましょう。
AfterEach
これは、JUnit5用のアノテーションで、各テストメソッドの実行後に差し込みたい処理を記述します。
ここでは、EntityManagerのflushメソッドを明示的に呼び出しています。
flushメソッドは、永続性コンテキストにあるエンティティをデータベースのレコードと同期させるための処理を行なっています。
通常、このメソッドは意識せずとも、トランザクションがコミットされるタイミングで自動的に呼ばれるようになっています。 参考しかし、今回のテストコードでは、データベース操作が終わったらデータベースを元に戻すため、
RollBack
する必要があります。すると、flushメソッドが呼ばれなくなってしまうため、テストメソッドの期待結果がデータベースに反映されず、テストが通らない、という不具合が起こってしまいます。
いくつか対処する方法はありますが、アプリの実行と同じように、各メソッドでトランザクションがコミットされる、すなわち、処理が完了したタイミングでflushメソッドを明示的に呼び出すのがよいかと思われます。以上より、各テストメソッドの実行後にflushメソッドを呼び出すことで、期待結果の検証を正しく行うことができるようになります。
ExpectedDatabase
DatabaseSetupアノテーションと同時に用いられ、名前の通り、テストメソッドの実行後のデータベースの状態を検証するためのものです。
DatabaseSetupアノテーションと同様、value値に期待結果のテーブル状態を記述したCSVファイルが格納されているディレクトリを指定してあげます。
更に、「assertionMode」というプロパティが設定されていますが、ここに「NON_STRICT」を設定することで、全てのカラムではなく、CSVファイルで指定したカラムのみを検証してくれるようになります。Transactionalアノテーション
このテストクラスでは、
Transactionalアノテーション
をクラスレベルで付与しています。
クラスレベルでこのアノテーションを設定した場合、クラス内の全てのメソッドに対してアノテーションを付与したことと同様となります。
Controllerのテストではトランザクション制御が必要ないものもあったりしますが、各メソッドに毎回設定しようとすると抜け漏れが出てしまうので、クラスレベルで一括で設定しておくのがよいでしょう。
これでCRUD処理に必要な処理はある程度理解できたかと思います。
レベル2の最後に、(私がハマりまくったので)データベース操作をテストする際の注意点をいくつか記載しておきます。何かの参考になれば幸いです。update/delete処理の記述方法について
上記の処理は、既存のレコードを書き換える処理です。
複数レコードに対して処理することもあることは思いますが、Webアプリケーションでは多くの場合、一つのレコードを対象とするかと思います。
このとき、処理対象を明確にするため、キー情報が必要となります。やり方は色々あるかと思いますが、「CSVファイルのレコードへIDも明示しておく」のがシンプルに書きやすいかと思います。
ここでの注意点としては、テストを実現するためのロジックの考え方についてです。少し長くなりそうということから、補足に記載しましたので、興味があったら見てみてください。
補足: テストをいかに実現するか
さて、上記の、「キー情報」を取得する方法について、テストを書きやすくするためにテスト用のユーティリティクラスを用意したり、果てはテストしやすいようアプリ側のコードへメソッドを追加する、というようなやり方もできないことはないですが、あまり推奨はされません。
まず、テストを書きやすくするためのユーティリティクラスを作ることについて、テストを書くのに行き詰まった場合、考えるべきはいかにテストを通すか
ではありません。
もっとシンプルに書けないか
ということについて思考すべきです。
簡単なCRUD処理レベルからテスト用のメソッド・クラスを作成していると、実務レベルのアプリケーションでは、テストコードがどのようなことになるかは想像に難くないかと思います。
また、しばしば、テストを書きやすいコードはいいコードだ、というように言われることがあります。
これは、アプリ側でテストを通すことを書くことを考えて書くべき、という話ではなく、「個々のモジュール同士が密結合となり、テスト時の依存解決が困難になる」といったことを防ぐべきというものです。こういった実装はテストがしづらいだけでなく、拡張・変更の際にも影響範囲が広がり、予期せぬバグを生み出すことも繋がってしまいます。
テストコードに注力していると実装よりもテストコード寄りになり、逆もまた然りで最初の内は中々気づきにくい部分ではありますが、ひと呼吸置いて、基本に立ち返ってみると、よいかもしれません。
エラそうに書きましたが、私自身テストコードを書いていてハマった部分なので、自戒も兼ねて書いておきます。
create処理のID値について
テストメソッドで新規レコードをデータベースに登録する場合について考えてみます。
例えば、IDが自動採番されるものであった場合、セットアップのレコードにIDが割り振られていると、キー重複を起こしてしまうことがあります。
※というか大体重複します):それなら、結果セットのレコードも採番されるIDを設定しておけば...ともなりましたが、自動採番される値はへたにコントロールしない方が無難だったりします。
解決策としては、新規レコードの生成を検証したい場合、IDを除いたCSVファイルを利用し、IDの関与しない状態で中身のカラムのみを検証する、という方針で進めるとよいかと思います。
レベル2も新しいことがたくさん出てきましたね。
ですが、ある程度慣れてしまえばサクサク書くこができますし、何よりも手動テストのようにサーバーを起動して、ページへアクセスして、実際に処理してDBを見に行って...とすることなく検証ができるので、非常に恩恵の大きい部分だと思います。
ですので、レベル2までの範囲をマスターしておくだけでも、開発時の不具合修正の効率がグッと上がる...はずです。レベル2をこなすことによって、以下のことが可能となるはずです。
- CSVファイルによってテストコードの実行前後のデータベースの状態を定義できる
- データベースのCRUD処理を検証することができる
- データベースに関するテストコードで必要なアノテーションの概要が理解できる
さて、Spring BootはWebアプリケーションを作成するためのフレームワークなので、実際にデータベースを操作する際には、POSTリクエストを利用するのが一般的です。
そのため、レベル3ではPOSTリクエストの検証について見ていきたいと思います。
レベルこそ上がりますが、これまでの知識で十分に理解できるものなので、最後までついてきて頂けるとうれしいです(╹◡╹)※RESTful APIに関しては今回は対象外とします。
Level3. POSTリクエストをテストする
続いて、POSTリクエストを検証する際のテストコードについて見ていきます。
検証対象コード全体を載せると長くなってしまうので、ここではアプリの概要のみを記述し、テストコードへ注力したいと思います。レベル3では、題材として、TODOリストを利用します。
以下のような、よくある簡単なCRUD処理ができるシンプルなものです。POSTリクエストに関する新しい知識も多少は出てきますが、これまでの知識があれば理解できるものとなっているので、本記事の総復習がてら、テストコードを見ていただけたらと思います。
以下へ実際のテストコードを記載します。
多少長めとなっていますが、大半は理解できる...とうれしいです。TodoControllerTest.java@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @AutoConfigureMockMvc @SpringBootTest(classes = {DbMvcTestApplication.class}) @Transactional public class TodoControllerTest { //mockMvc TomcatサーバへデプロイすることなくHttpリクエスト・レスポンスを扱うためのMockオブジェクト @Autowired private MockMvc mockMvc; @Autowired private TodoDao todoDao; @AfterEach void tearDown() { todoDao.getEm().flush(); } /** * viewが正しく返されるか検証 * @throws Exception */ @Test void init処理でviewとしてtodoが渡される() throws Exception { this.mockMvc.perform(get("/todo/init")) .andExpect(status().isOk()) .andExpect(view().name("todo")); } /** * モデルへDBから取得したレコードが設定されたか検証する * 今回は複雑な処理でもないので、DBの中の1レコードがモデルに渡されていれば正常に動作しているとみなした * * @throws Exception */ @Test @DatabaseSetup(value = "/TODO/setUp/") void init処理で既存のタスクがモデルへ渡される() throws Exception { // mockMvcで「/todo/init」へgetリクエストを送信 this.mockMvc.perform(get("/todo/init")) // モデルへDBのレコードがリストとして渡される .andExpect(model().attribute("todoForm", hasProperty( "todoList", hasItem( hasProperty( "task", is("task1") ) ) ))); } /** * 画面の入力から新規レコードがDBへ登録されるか検証 * @throws Exception */ @Test @DatabaseSetup(value = "/TODO/setUp/create") @ExpectedDatabase(value = "/TODO/create/", assertionMode=DatabaseAssertionMode.NON_STRICT) void save処理で新規タスクがDBへ登録される() throws Exception { this.mockMvc.perform(post("/todo/save") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("newTask", "newTask")); } /** * 画面の入力で既存レコードが更新されるか検証 * 今回は画面情報を利用しないので、対象の自動採番されるIDを取得することができない。 * そのため、今回はアップデート対象を手動で指定する。 * 基本的には、リストの順番は保証されないので、SELECT時にソートしておく等の処理は必要になると思われる * @throws Exception */ @Test @DatabaseSetup(value = "/TODO/setUp/") @ExpectedDatabase(value = "/TODO/update/", assertionMode=DatabaseAssertionMode.NON_STRICT) void update処理で既存タスクが更新される() throws Exception{ // mockMvcで「todo/update」へpostリクエストを送信 long updateTargetId = 3L; int updateTargetIndex = 2; this.mockMvc.perform(post("/todo/update/" + updateTargetIndex + "/" + updateTargetId) .param("todoList[" + updateTargetIndex + "].task", "task3mod") .contentType(MediaType.APPLICATION_FORM_URLENCODED) ); } /** * 画面で選択したタスクが削除されるかどうか検証する * @throws Exception */ @Test @DatabaseSetup(value = "/TODO/setUp/") @ExpectedDatabase(value = "/TODO/delete/", assertionMode=DatabaseAssertionMode.NON_STRICT) void delete処理で既存タスクが消去される() throws Exception { long deleteTargetId = 3L; this.mockMvc.perform(post("/todo/delete/" + deleteTargetId) .contentType(MediaType.APPLICATION_FORM_URLENCODED) ); } }以下では、新しく登場したPOSTリクエスト関連のメソッドや、POSTリクエストを扱う際の注意点について記述していきます。やっていること自体はパラメータを設定してリクエストを投げているだけなので、こわくないと思います。
POSTのパラメータ
POSTリクエストのテストコードで特筆すべき点としては、リクエストに渡されるパラメータの設定方法があります。
といっても、POSTリクエストがある程度理解できていれば、直感的に設定することができます。
POSTリクエストについては、われらがMDNさんに分かりやすく書かれているのでおすすめです。さて、
MockMvc
を利用したテストコードでは、レベル1でperformメソッドでGETリクエストを行なっていた部分を、POSTリクエストに変えてあげます。
その後、paramメソッドでkey-value形式でパラメータを渡すことができるので、実際のリクエストで渡される形に合わせてリクエストを作ってあげるだけでOKです。
GETリクエストでもparamメソッドを呼び出すことはできますが、その場合は、クエリパラメータとして送信されます。ここでは、フォームをもとにPOSTリクエストを送信するので、パラメータはリクエストボディに格納されます。また、contentTypeは指定しなくても動くには動きますが、実際のPOSTリクエストになるべく近づけておくためにも、設定しておくのがよいかと思います。
POSTリクエストをテストする場合の注意点
今回は、POSTリクエストによって、データベースが正しく更新されるか、といったことを中心に検証しています。
このとき、問題になるのは、POSTリクエストによって処理される対象です。
各CRUD処理についてざっくりと見ておきたいと思います。
- 新規作成
レコードを新規で作成する場合、新規レコード用のパラメータはテーブルとは独立しているので、特に気にすることはないかと思います。
- 読み出し
今回はViewにview名が渡されるまでが検証範囲となっているので、Modelへ渡されるオブジェクトの検証さえできればOKです。よって、ここも特に気にするようなことはない...はずです。
- 削除
削除対象のIDはリクエストのパスによって判定されるので、MockMvcでリクエストを作成する際にも、決め打ちで削除対象を明示しておく必要があります。
こういった決め打ちは「実装」では避けるべきですが、テストコードではあまり気にする必要はないかと思います。
そもそもテストコードではデータベースの状態を「決め打ち」としているので、変更・拡張を想定するよりは、一定の入力から一定の出力が常に得られるか
という部分に注力するべきでしょう。
何度実行しても常に同じ結果が得られるのがテストコードの強みなので、実装とは書き方についても分けて考えるべきかなと個人的には思っています。
- 更新
更新処理についても同様のことが言えます。
ただし、更新する際、エンティティのリストのうち、一レコードを対象としたい場合、編集対象をリストから切り離して個別の更新用エンティティに格納しておく等、多少の工夫が必要になります。
今回の更新処理では、リストのインデックス・エンティティのIDを決め打ちとしていますが、リストのオーダーは基本的には保証されてはいないので、業務レベルのアプリケーションでは上記のような形で、「常に同じ入力から同じ結果が得られる状態」を作っておく必要があります。
その辺の話は、画面系のテストコードにも慣れてきたら書いてみたいなーと思います(願望)
レベル3と銘打ってはいましたが、大半の部分は今までの総復習の形となっていたので、分かる...分かるぞ...!!となって頂けていたらとてもうれしいです(:
レベル3のテストコードを理解することで、以下のことが可能となります。
- POSTリクエストについても、データベースが正常に更新されたか検証することができる
- MVCのModel、Controllerのレイヤーについて、簡単なテストコードを書くことができる
まとめ
思った以上に長くなってしまいましたが、これで簡単なCRUD処理のテストコードの書き方について、見ていくことができました。
普段実装部分を書く上では意識しないようなところまで意識する必要があり、中々ハードな部分もあったかと思います。ですが、テストコードを書くことによって、フレームワーク・言語への理解が深まり、開発も効率化することができ、たくさんの恩恵が得られます。そして、何より、テストが通ったときの喜びは実装だけでは味わうことのできないもので、最初の壁を乗り越えれば、テストコードを書くことはとても楽しくなります。
本記事を通して、少しでもSpring Bootでテストコード書けそうかも...?と思っていただけましたら幸いです。
まだまだ私自身テストコードに関しては未熟なので、テストコードに関する解説なんかがもう少し増えてくれたらいいなーと思います。(この記事だけでけっこう体力を使い果たしたので、ログイン処理に関するテストコードの記事は少し休んでから書いていきますƪ(˘⌣˘)ʃ)