LoginSignup
1
1

LibertyによるWebサービスアプリ開発メモ: (5)JUnitによる単体テスト

Last updated at Posted at 2017-10-18

はじめに

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に変更するのをお忘れなく。)

pom.xml
<?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のテスト・コード用のフォルダを作っておきます。

image.png

サンプルWebサービスアプリの作成

テストの対象となるWebサービスアプリを作成します。
これまでの記事で記載していた以下の処理を含む単純なアプリの想定です。

  • JAX-RSベースのWebサービス
  • JPA経由でのDBアクセス
  • SLF4J+Logbackによるロギング処理

DBのテーブルもLibertyによるWebサービスアプリ開発メモ: (3)JPA経由でのDBアクセスで準備したものをそのまま使う想定です。

アプリケーション

HelloWorldAppConfig.java
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;
  }
}
HelloWorldResource01.java
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;
  }
}
HelloWorldJPA01.java
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用

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">
  	<non-jta-data-source>jdbc/DB2Sample</non-jta-data-source>
  	<class>com.ibm.jaxrs.sample.jpa01.Account</class>
  </persistence-unit>
</persistence>

Logback用(デフォルト構成のまま)

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="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

ここまでのプロジェクトの階層構造はこんなイメージです。
image.png

ここまでは、アプリをゴリゴリと開発してきたというイメージです。(これまでの記事の通り。)

JUnitによる単体テスト

さて、ここからが当記事の主題で、上のアプリをJUnitでテストするというステップです。

サービス実装をPOJOとしてテスト

JAX-RSをベースにWebサービスを作成すると、そのロジック部分はPOJOの1メソッドのようなイメージで実装されることになりますので、それをそのままPOJOとして単体テストすることができます。

HelloWorldResource01クラスのsayHelloWorld()メソッドをテストするため、src/test/javaフォルダに、以下のようなテスト用のクラスを作ってみます。

HelloWorldResouce01Test.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ビューでグリーンのラインになっているのでテストが成功したことが確認できます。
image.png

同様に、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テスト実行をすると、全テストメソッドが実行されます。
image.png

コンソールには以下のような出力がされています。

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()メソッドをテストするためのクラスを作成します。

HelloWorldJPA01Test.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.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に持たせるということになります。
こんな感じです。

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に追加します。

image.png

この構成を使用してJUnitテストを実行すると...

image.png

console
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という順序で探索されます。(見つからなかったらデフォルトの設定が使われる)

logback-test.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のテストを流してみると...

console
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と組み合わせていろいろ制御ができるようです。その辺りはまた追々...

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1