はじめに
Webサービス(REST)アプリを開発してLiberty上で動かすまでの流れについて、試したことを備忘録として記載していきます。
今回はJunitについて。
JAX-RSをベースに実装されたWebサービスアプリ(JPA経由でのDBアクセスを含む)を、JUnitを使ってテストするってのをやってみます。
アプリ開発においてここは結構肝になる所だと思います。
※この連載は、実際に一通り動かして手順を確認しているもので、各技術要素の細かな解説はしていませんのであしからず。(個別の技術についてはそれぞれ情報は散在していますが、一気通貫でやろうとすると結構つまづくこと多いので、その辺を主眼にして記載しています。)
ちなみにWebSphere Libertyは最近オープンソース化されましたね!
参考: Open Liberty
#関連記事
LibertyによるWebサービスアプリ開発メモ: (1)環境構築
LibertyによるWebサービスアプリ開発メモ: (2)MavenプロジェクトによるJAX-RSアプリ開発
LibertyによるWebサービスアプリ開発メモ: (3)JPA経由でのDBアクセス
LibertyによるWebサービスアプリ開発メモ: (4)ロギング
LibertyによるWebサービスアプリ開発メモ: (5)JUnitによる単体テスト
LibertyによるWebサービスアプリ開発メモ: (6)Eclipse-GitHub連携
プロジェクトの設定
例によってMavenプロジェクトをベースにカスタマイズします。
前の記事で作成した、Mavenプロジェクトをベースとして新規プロジェクトを作成し、JAX-RS、JPA、SLF4J/Logbackを依存関係として組み込み、さらにJuni関連の依存関係を組み込むことにします。
プロジェクトの作成はこちら「LibertyによるWebサービスアプリ開発メモ: (2)MavenプロジェクトによるJAX-RSアプリ開発 」を参考に。
依存関係(pom.xml)
pom.xmlに以下のdependencyを追加します。(Javaのsource/targetも1.7=>1.8に変更するのをお忘れなく。)
<?xml version="1.0" encoding="UTF-8"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>https://raw.github.com/WASdev/ci.maven.tools/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<modelVersion>4.0.0</modelVersion>
<groupId>test.sample</groupId>
<artifactId>MavenLibertyTest03</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>MavenLibertyTest03</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>net.wasdev.maven.tools.targets</groupId>
<artifactId>liberty-target</artifactId>
<version>17.0.0.2</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.persistence/eclipselink -->
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.6.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-core -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
pom.xmlを変更したら、プロジェクトを右クリック - [Maven] - [Update Project]を選択し、project configurationのアップデートを行っておきましょう。
フォルダの作成
src以下に、JPAやLogback用のresoucesと、JUnitのテスト・コード用のフォルダを作っておきます。
サンプルWebサービスアプリの作成
テストの対象となるWebサービスアプリを作成します。
これまでの記事で記載していた以下の処理を含む単純なアプリの想定です。
- JAX-RSベースのWebサービス
- JPA経由でのDBアクセス
- SLF4J+Logbackによるロギング処理
DBのテーブルもLibertyによるWebサービスアプリ開発メモ: (3)JPA経由でのDBアクセスで準備したものをそのまま使う想定です。
アプリケーション
package com.ibm.jaxrs.sample;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/rest/*")
public class HelloWorldAppConfig extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<Class<?>>();
classes.add(com.ibm.jaxrs.sample.HelloWorldResource01.class);
classes.add(com.ibm.jaxrs.sample.HelloWorldJPA01.class);
return classes;
}
}
package com.ibm.jaxrs.sample;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/helloworld")
public class HelloWorldResource01 {
Logger logger = LoggerFactory.getLogger(HelloWorldResource01.class);
@GET
@Path("/sayHelloWorld")
public String sayHelloWorld() {
return "Hello World!";
}
@GET
@Path("/getQueryParam")
@Produces("application/json")
public Response getQueryParam(@DefaultValue("XXX") @QueryParam("name1") final String name1,
@DefaultValue("YYY") @QueryParam("name2") final String name2) {
logger.info("***getQueryParm***");
logger.debug("LogTest: name1={}, name2={}", name1, name2);
Response response = null;
Map<String, String> entity = new HashMap<String, String>();
entity.put(name1, name2);
response = Response.status(Response.Status.OK).entity(entity).build();
return response;
}
}
package com.ibm.jaxrs.sample;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import com.ibm.jaxrs.sample.jpa01.Account;
@Path("/helloworldjpa")
public class HelloWorldJPA01 {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPATest01");
EntityManager em = emf.createEntityManager();
@GET
@Path("/jpa01")
@Produces("application/json")
public Response getRecord() {
System.out.println("***Begin getRecord()***");
List<Account> accounts =
em.createQuery("select a FROM Account a", Account.class).getResultList();
// em.close();
// emf.close();
Response response = Response.status(Response.Status.OK).entity(accounts).build();
return response;
}
@POST
@Path("/jpa01")
// @Consumes("application/json")
@Produces("application/json")
public Response insertRecord(
@DefaultValue("X000") @QueryParam("empNumber") final String empNumber,
@DefaultValue("000") @QueryParam("data01") final String data01,
@DefaultValue("999") @QueryParam("data02") final String data02) {
System.out.println("***Begin insertRecord()***");
Account account = new Account();
account.setEmployeeNumber(empNumber);
account.setData01(data01);
account.setData02(data02);
System.out.println("***invoke persist()***");
em.getTransaction().begin();
em.persist(account);
em.getTransaction().commit();
Response response = Response.status(Response.Status.OK).entity(account).build();
return response;
}
}
設定ファイル
JPA用
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="JPATest01" transaction-type="RESOURCE_LOCAL">
<non-jta-data-source>jdbc/DB2Sample</non-jta-data-source>
<class>com.ibm.jaxrs.sample.jpa01.Account</class>
</persistence-unit>
</persistence>
Logback用(デフォルト構成のまま)
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
ここまでは、アプリをゴリゴリと開発してきたというイメージです。(これまでの記事の通り。)
JUnitによる単体テスト
さて、ここからが当記事の主題で、上のアプリをJUnitでテストするというステップです。
サービス実装をPOJOとしてテスト
JAX-RSをベースにWebサービスを作成すると、そのロジック部分はPOJOの1メソッドのようなイメージで実装されることになりますので、それをそのままPOJOとして単体テストすることができます。
HelloWorldResource01クラスのsayHelloWorld()メソッドをテストするため、src/test/javaフォルダに、以下のようなテスト用のクラスを作ってみます。
package com.ibm.jaxrs.sample;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.HashMap;
import javax.ws.rs.core.Response;
import org.junit.Test;
public class HelloWorldResource01Test {
@Test
public void sayHelloWorldTest() {
String expected = "Hello World!";
HelloWorldResource01 sut = new HelloWorldResource01();
String actual = sut.sayHelloWorld();
assertThat(actual, is(equalTo(expected)));
}
}
このコードでは、HelloWorldResouce01クラスをインスタンス化(sut)して、sayHelloWorld()メソッドを呼び出して結果をassertThat()により検証しています。
(※sut: System Under Testの略らしい...。これがテスト対象だよという意味ですね。)
ここでは"Hello World!"という固定の文字列が返されることが期待されるので、それが返って来たかどうかをチェックしている訳です。
このテスト用メソッドを右クリック - [Run As] - [JUnit Test]で、JUnitによるテストが実行されます。
JUnitビューでグリーンのラインになっているのでテストが成功したことが確認できます。
同様に、getQueryParamTest()用のテストメソッドも上のクラスに追加してみます。
テスト対象のgetQueryParamTest()メソッドは、Webサービスとしては、以下のようなリクエストを発行した場合に、
GET http://localhost:9080/MavenLibertyTest03/rest/helloworld/getQueryParam?name1=aaa&name2=bbb
以下のようなJSONの結果が返されるサービスを想定したものです。
{"aaa":"bbb"}
HTTPリクエストのQueryストリングからメソッドのパラメータへの変換や、JSONデータへのフォーマット変換は、実際にはJAX-RSやJacksonのランタイム(今回の想定ではそれらの実装を提供しているのはLiberty)によって行われることになります。
ここではLiberty上での稼働ではなく、ロジックの単体テストを想定しているため、HTTPリクエストやJSONなどは意識せず、引数やResonseオブジェクトのレベルで検証をするイメージとなります。
...
@Test
public void getQueryParamTest() {
String name1 = "aaa";
String name2 = "bbb";
int expected = 200;
HelloWorldResource01 sut = new HelloWorldResource01();
Response response = sut.getQueryParam(name1, name2);
if (response.getEntity() instanceof HashMap) {
HashMap<String, String> responseEntity = (HashMap<String, String>) response.getEntity();
System.out.println(name1 + ":" + responseEntity.get(name1));
}
System.out.println("className: " + response.getEntity().getClass().getName());
int actual = response.getStatus();
assertThat(actual, is(expected));
}
...
上の例ではアサーション部分は手を抜いているのでResponseオブジェクトのステータスコードに200(正常終了)が返って来た事のみを検証しています。
(ここでは戻されたHashMapオブジェクトを取得して結果を目視できるよう短絡的に標準出力に書き出すコードをつけてますが、本来であればその辺もアサーションに組み込むなど工夫は必要と思います。)
クラスの単位でJUnitテスト実行をすると、全テストメソッドが実行されます。
コンソールには以下のような出力がされています。
22:52:42.990 [main] INFO com.ibm.jaxrs.sample.HelloWorldResource01 - ***getQueryParm***
aaa:bbb
className: java.util.HashMap
※HTTPリクエストのハンドリングや、JSON形式のデータの検証なども含めて行いたい場合は、当然その辺りの処理を行ってくれるLibertyランタイム上で稼働させた上でテストをする必要があります。それは次の段階のテストということになるので、別の記事で取り上げる予定です。ここはあくまで単体テストを想定。
JPA経由のDBアクセスを含むコードの単体テスト
JUnitによる単体テストを行う場合、テスト用のコードは基本的にスタンドアローンのJavaアプリケーションのイメージで実行されます。JavaEEアプリの場合はJavaEEサーバーのランタイムが提供するサービスを利用して稼働する部分がありますので、ランタイム依存のコードをテストするのが単体テストレベルだとちょっとやっかいです。DBアクセスもその1つと言えます。
ただし、JPAはJavaEE環境だけでなくJavaSE環境でも動作するため、DBアクセスにJPAを使用している場合はJUnitによる単体テストがやりやすくなります。
テスト・クラスの作成
src/test/javaに以下にHelloWorldJPA01.getResource()メソッドをテストするためのクラスを作成します。
package com.ibm.jaxrs.sample;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.Vector;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.ws.rs.core.Response;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.ibm.jaxrs.sample.jpa01.Account;
public class HelloWorldJPA01Test {
private EntityManagerFactory emf;
private EntityManager em;
private HelloWorldJPA01 sut = new HelloWorldJPA01();
@Before
public void setUp() throws Exception {
emf = Persistence.createEntityManagerFactory("JPATest01");
em = emf.createEntityManager();
}
@After
public void tearDown() throws Exception {
em.close();
}
@Test
public void getRecordTest() {
int expected = 200;
Response response = sut.getRecord();
if (response.getEntity() instanceof Vector<?>) {
Vector<Account> responseEntity = (Vector<Account>) response.getEntity();
Account account = (Account) responseEntity.get(0);
System.out.println("id:" + account.getId());
System.out.println("EmployeeNumber:" + account.getEmployeeNumber());
System.out.println("id:" + account.getData01());
}
System.out.println("className: " + response.getEntity().getClass().getName());
int actual = response.getStatus();
assertThat(actual, is(expected));
}
}
テスト対象のHelloWorldJPA01.getResource()では、JPAアクセスを行っているためにEntityManagerFactory、EntityManagerオブジェクトを使用しています。そのため、テストメソッドの前後でそれらの前処理、後処理をしています。
テスト用persistence.xmlの作成
次に、テスト用のリソースとして、src/test/resouces/META-INF/フォルダ以下にpersistence.xmlを作成します。
先に作成した本番用の設定としては、JavaEEサーバー(Liberty)上にDBとの接続構成をデータソースとして登録している想定で、persistence.xmlからはそのデータソースを参照していました。
単体テストではJavaEEサーバーが無いので、persistence.xml上にDBへの接続情報を個別に持たせるようにします。単体テストで利用できるDB環境を用意しておいて、そこに対する接続情報をテスト用のpersistence.xmlに持たせるということになります。
こんな感じです。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="JPATest01" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>com.ibm.jaxrs.sample.jpa01.Account</class>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:db2://DB2Server01:50000/SAMPLE"/>
<property name="javax.persistence.jdbc.driver" value="com.ibm.db2.jcc.DB2Driver"/>
<property name="javax.persistence.jdbc.user" value="db2admin"/>
<property name="javax.persistence.jdbc.password" value="xxxxxxxx"/>
</properties>
</persistence-unit>
</persistence>
※この例では、先に定義した本番用persistence.xmlから参照しているデータソースと宛先は同じものを指定していますが、別のDB環境をテスト用に使い分けすることも出来るわけです。
JUnitの実行構成
本番環境ではDBへの接続はJavaEEサーバー(Liberty)が担うことになるので、アクセス用のJDBCドライバー(jar)もJavaEEサーバー側で設定/ロードされればよいことになります。
ですが、単体テストではJavaEEサーバーがいないので、JDBCドライバーをロードできるようにクラスパスに追加してあげる必要があります。
テスト用クラスを右クリック - [Run As] - [Run Configurations...]でJUnitの実行構成を作成します。ここで、使用するJDBCドライバーのjar(この例ではDB2用のType4 JDBCドライバーのjar: db2jcc4.jar)をClasspathに追加します。
この構成を使用してJUnitテストを実行すると...
INFO: Found persistence provider "org.eclipse.persistence.jpa.PersistenceProvider". OpenJPA will not be used.
[EL Info]: 2017-10-17 23:51:52.152--ServerSession(1812995265)--EclipseLink, version: Eclipse Persistence Services - 2.6.3.v20160428-59c81c5
[EL Info]: connection: 2017-10-17 23:51:53.713--ServerSession(1812995265)--/file:/C:/y/workspace/workspaece_oxygen_test01/MavenLibertyTest03/target/test-classes/_JPATest01 login successful
***Begin getRecord()***
INFO: Found persistence provider "org.eclipse.persistence.jpa.PersistenceProvider". OpenJPA will not be used.
id:1
EmployeeNumber:A001
id:AAA
className: java.util.Vector
こんな感じでJPAを含むコードの単体テストを行うことができました。
JPAを使っていると、このように単体テストが結構シンプルにできるのが良いですね。
ログレベルの変更
JPAのテストで、本番用とテスト用でpersistence.xmlをそれぞれ分けて管理したように、Logback用の設定ファイル(logback.xml)もテスト用に個別に管理することができます。
これも、src/test/resourcesにlogback.xmlを作成しておけば、JUnitのテスト時にはこちらのテスト用の構成が使われることになります。
例えば本番用ではログ出力はなるべく抑えるためにINFOやWARNなどのレベルにしておいて、テスト時にはなるべく多くの情報を出力させるためにDEBUGレベルにする、といった切り替えを行うと思います。
先に作成した本番用のlogback.xmlでは、ルートのログレベルをINFOにしていましたので、本番用logback.xmlをsrc/test/resourcesにlogback-test.xmlという名前でコピーして、ログレベルをDEBUGに変更してみます。
設定ファイルはクラスパス上から、logback.groovy, logback-test.xml, logback.xmlという順序で探索されます。(見つからなかったらデフォルトの設定が使われる)
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
この状態で、最初に実行したHelloWorldResource01Testのテストを流してみると...
23:07:34.565 [main] INFO com.ibm.jaxrs.sample.HelloWorldResource01 - ***getQueryParm***
23:07:34.565 [main] DEBUG com.ibm.jaxrs.sample.HelloWorldResource01 - LogTest: name1=aaa, name2=bbb
aaa:bbb
className: java.util.HashMap
先ほど出ていなかったDEBUGメッセージが出力されるようになっています!
おわりに
単体テストが済んだら次の段階としては実際のJavaEEサーバー環境にデプロイした状態でのテストを行いたくなると思いますが、Mavenと組み合わせていろいろ制御ができるようです。その辺りはまた追々...