JUnitでモックを利用したテストコード（EasyMock中心）

JUnitでモックを使ったテストについてです。
職場でテストコードを普及させるための記事2回目になります。
EasyMockについてが中心となるので、あまり最近の話ではありません。

## 実行環境

* Java 1.8
* JUnit 4.4
* EasyMock 2.4
* Seasar 2.4.46


## モックとは
>モックオブジェクト（Mock Object）とは、ソフトウェアテスト時、特にテスト駆動開発、ビヘイビア駆動開発における代用の下位モジュールスタブの一種。スタブと比較して、検査対象のモジュール
がその下位モジュールを正しく利用しているかどうかを検証するのに使われる。

モックオブジェクト 出典: フリー百科事典『ウィキペディア（Wikipedia）』

使い方としては、例えば以下のようにHelloJunitというクラスを作成しているとします。
このクラスをテストしようとした際に、もしGreetDaoクラスができていない（あとはDaoで参照しているDBが用意されていないとか）とした場合に、普通にテストしてしまうとエラーになってしまうと思います。

```java:HelloJunit
public class HelloJunit {

	@Binding
	GreetDao greetDao;

	public String sayGreeting() {
		return greetDao.getGreet();
	}
```

本来はHelloJunitクラスをテストしたいだけなので、依存先（GreetDao）のコードができていなくてもテストできるべきだよねということで、ひとまず動くようにハリボテのクラスを用意して動かせるようにしようよ。というのがモックの役割になります。

## どうやってモックを作るのか
以下のような方法があります。

* モックライブラリを利用
* モックを作成したいクラスを継承してテスト用に作成する

モックライブラリとしては、`Mockito`、`JMockit`、そして`EasyMock`、`MockInterceptor`などがあります。

最近では`Mockito`がよく使われている印象です。

Seasar2では`EasyMock`、`MockInterceptor`がデフォルトで入っています。
いまだにSeasar2を利用しているような環境で新しいライブラリを入れるのは難しいと思いますので、今回はEasyMockで進めたいと思います。

## EasyMockを利用したテストコードサンプル
### とりあえずコード

プロダクトコードについて上で紹介した内容から少し変更しています。 
*理由については後ほど説明します。*

```java:プロダクトコード
public class HelloJunit {

	GreetDao greetDao;
	
	public HelloJunit(GreetDao greetDao) {
		this.greetDao = greetDao;
	}

	public String sayGreeting() {
		return greetDao.getGreet();
	}
}
```

テストコードはこちらです。

```java:テストコード
import static org.easymock.EasyMock.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.seasar.framework.unit.annotation.EasyMock;
import org.seasar.sastruts.example.dao.GreetDao;

public class HelloJunitTest {

	HelloJunit helloJunit;

	@EasyMock
	GreetDao greetDao;

	@Before
	public void setUp() {
		greetDao = createMock(GreetDao.class);
		helloJunit = new HelloJunit(greetDao);
	}

	@Test
	public void 挨拶を言う() throws Exception {
		// setup（事前準備）
		expect(greetDao.getGreet()).andReturn("Hello");
		replay(greetDao);

		// exercise（実行）
		String actual = helloJunit.sayGreeting();

		// verify（検証）
		String expected = "Hello";
		assertThat(actual, is(expected));
		verify(greetDao);

		// tear down（後処理）
		// 何かあれば・・・
	}
}
```

なんとなくわかると思いますが解説します。

#### EasyMockアノテーション
メンバー変数にGreetDaoを用意しているのですが、`EasyMockアノテーション`がついています。
これをつけることでEasyMockを利用可能になります。

#### createMock
setUpメソッドでは`createMock`メソッドを利用して、greetDaoを作成しています。
ここで作成したgreetDaoをテスト対象メソッドであるhelloJunitのインスタンス生成時に渡してあげることで、モックがセットされることになります。

#### テストメソッド内のsetup
setup（事前準備）というコメントの中では、`expect`メソッドを利用しています。
ここがモックの挙動を決める一番重要な部分です。

greetDao.getGreet()がこういう呼び出し方をされた場合、〜を返すよというのを設定しています。
`expected()`でメソッドの呼び出し方を指定。その後の`andReturn()`で戻り値を設定します。
今回の場合は、引数なしのgreetDao.getGreet()を呼び出した場合に、"Hello"を返すという設定をしています。

`replay`というメソッドは直前で設定したモックの挙動を記憶するというイメージです。
expected()を実行したらreplayをするというようにしてください。

#### exercise
ここではテストをしたいメソッドを実行して、変数に格納しています。
格納する変数はどのような型であっても`actual`に統一するとわかりやすいと言われています。

#### verify
ここでは、`helloJunit.sayGreeting();`の戻り値と期待値を比較しています。
また、`verify(greetDao);`というのは、setup内で指定したモックの挙動が想定通り呼び出されたかを検証します。
もし`helloJunit.sayGreeting();`が1回も呼び出されていなければエラーが返却されます。

### tear down
ここはテスト後の後処理を行います。
例えばテストデータをDBに投入していたとした場合、そのデータを削除したりを行います。


## EasyMockの利用方法

### 引数をチェックする

今回はgreetDaoのfindGreetメソッドが想定通りの呼び出され方をしているか確認します。

```java:テスト対象コード
public String sayGreeting() {
	return greetDao.findGreet(1);
}
```

`expect()`内に期待しているメソッドの呼び出し方をそのまま書きます。
今回の場合は、`findGreet`の引数に1が入ることを想定しているので、そのまま`findGreet(1)`を記載します。


```java:テストコード
@Test
public void 引数テスト() throws Exception {
	// setup（事前準備）
	expect(greetDao.findGreet(1)).andReturn("Hello");
	replay(greetDao);

	// exercise（実行）
	String actual = helloJunit.sayGreeting();

	// verify（検証）
	verify(greetDao);
}
```

ちなみに、もし`findGreet(2);`で呼び出されていた場合には以下のようなエラーが出力されます。

```java:エラーの場合の参考
java.lang.AssertionError: 
  Unexpected method call findGreet(2):
    findGreet(1): expected: 1, actual: 0
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:32)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:61)
	at com.sun.proxy.$Proxy4.findGreet(Unknown Source)
	at org.seasar.sastruts.example.service.HelloJunit.sayGreeting(HelloJunit.java:17)
	at org.seasar.sastruts.example.service.HelloJunitTest.引数テスト(HelloJunitTest.java:52)
--以降略--
```

### 戻り値をセットして後続処理をテストする

`findGreet`の戻り値によって分岐を入れてみました。
今回のテストでは、パスタが返却されるパターンをテストしたいと思います。

```java:プロダクションコード
public String sayGreeting() {
	String greet = greetDao.findGreet(1);
	if (greet.contentEquals("Hello")) {
		return "パスタ";
	} else {
		return "ラーメン";
	}
}
```

戻り値を設定するには、`andReturn`に戻り値を指定します。
今回の場合は、"Hello"が返却されるようになります。

```java:テストコード
@Test
public void 引数テスト() throws Exception {
	// setup（事前準備）
	expect(greetDao.findGreet(1)).andReturn("Hello");
	replay(greetDao);

	// exercise（実行）
	String actual = helloJunit.sayGreeting();

	// verify（検証）
	String expected = "パスタ";
	assertThat(actual, is(expected));
	verify(greetDao);
}
```

### Void型メソッドをテストする

void型のメソッドをテストする方法です。


```java:プロダクションコード（GreetDao）
public interface GreetDao {
	public void insertGreet(String greet);
}
```

```java:プロダクションコード（HelloJunit）
public String addGreet() {
	greetDao.insertGreet("Good Night");
	return "OK";
}
```

void型をテストする場合は、テストコードの中で実際にvoidメソッドを実行します。
その後に`expectLastCall()`を呼び出します。
これによって、直前のメソッド実行を記憶して検証することができます。

ちなみに同一のvoidメソッドを二回実行している場合には、`expectLastCall().times(2);`というようにtimes()を付与することで、回数分実行されたかを検証します。

```java:テストコード
@Test
public void voidメソッドをテスト() throws Exception {
	// setup（事前準備）
	greetDao.insertGreet("Good Night");
	expectLastCall();
	replay(greetDao);

	// exercise（実行）
	String actual = helloJunit.sayGreeting();

	// verify（検証）
	verify(greetDao);
}
```

### EasyMockの注意点
EasyMockでは、インターフェースのクラスしかモックすることができません。
もし具象クラスをモックしたい場合は、次に紹介する継承する方法を試してください。

*補足:`org.easymock.classextension.EasyMock` を利用することで具象クラスもテスト可能ですが、古いバージョンのSeasarを利用している場合は入っていない可能性あります。*


## 継承してチェックする

テスト対象のメソッド内で利用しているクラスがインターフェースでなかったり、テストするには不都合があるような場合には、テストのために継承してモッククラスを自分で用意することもあります。

例えば、メソッドで返却されるBeanにequalsメソッドがオーバーライドされておらず、一つ一つのフィールドをチェックするのがめんどくさいような場合は、以下のようにequalsを継承してテストします。

```java:テスト対象のプロダクションコード
public TransferResultDto transfer(String payerAccountId,
										String payeeAccountId,
										String payerName,
										long transferAmount) {

		Balance payerBalance = balanceDao.findByAccountId(payerAccountId);
		if (payerBalance == null)
			throw new BusinessLogicException("残高が取得できません");

		if (transferAmount > payerBalance.amount)
			throw new BusinessLogicException("残高が足りません");

		Balance payeeBalance = balanceDao.findByAccountId(payeeAccountId);
		if (payeeBalance == null)
			throw new BusinessLogicException("振込先口座が存在しません");

		LocalDateTime transferDate = LocalDateTime.now();

		Transfer peyerTransaction = new Transfer();
		peyerTransaction.accountId = payerAccountId;
		peyerTransaction.name = payerBalance.name;
		peyerTransaction.transferAmount = -transferAmount;
		peyerTransaction.transferDate = transferDate;

		Transfer payeeTransaction = new Transfer();
		payeeTransaction.accountId = payeeAccountId;
		payeeTransaction.name = payeeBalance.name;
		payeeTransaction.transferAmount = transferAmount;
		payeeTransaction.transferDate = transferDate;

		transferDao.insertTransfer(peyerTransaction);
		transferDao.insertTransfer(payeeTransaction);
		balanceDao.updateAmount(payerAccountId, -transferAmount);
		balanceDao.updateAmount(payeeAccountId, transferAmount);

		Balance updatedPayerBalance = balanceDao.findByAccountId(payerAccountId);

		return new TransferResultDto(payerAccountId, payeeAccountId,
									 payerBalance.name, payeeBalance.name,
									 transferAmount,
									 updatedPayerBalance.amount);
	}
}
```

```java:戻り値のBean
public class TransferResultDto {

	private final String payerAccountId;
	private final String payeeAccountId;
	private final String payerName;
	private final String payeeName;
	private final long transferAmount;
	private final long amount;

	public TransferResultDto(String payerAccountId,
							 String payeeAccountId,
							 String payerName,
							 String payeeName,
							 long transferAmount,
							 long amount) {

		this.payerAccountId = payerAccountId;
		this.payeeAccountId = payeeAccountId;
		this.payerName = payerName;
		this.payeeName = payeeName;
		this.transferAmount = transferAmount;
		this.amount = amount;
	}

	public String getPayerAccountId() {
		return payerAccountId;
	}

	public String getPayeeAccountId() {
		return payeeAccountId;
	}

	public String getPayerName() {
		return payerName;
	}

	public String getPayeeName() {
		return payeeName;
	}

	public long getTransferAmount() {
		return transferAmount;
	}

	public long getAmount() {
		return amount;
	}

}
```

これをテストする場合には、テストクラス内で`TransferResultDto`を継承して`TransferResultDtoMock`クラスを作成します。
この`TransferResultDtoMock`クラスの中で全てのフィールドを比較するequalsメソッドを用意して、テストする際には`assertThat(actual, is(expected));`というように isで比較できるようになります。

*今回は、equalsメソッドを書くのがめんどくさいので、lombokを利用して自動生成しました*

```java:
public class TransferServiceTest {
	@Test
	public void 継承モックを試す() {
		// setup
		TransferResultDtoMock expected = 
						new TransferResultDtoMock("1", "2",
												ACCOUNT1_BEFORE_BALANCE.name, ACCOUNT2_BALANCE.name,
												2000, ACCOUNT1_AFTER_BALANCE.amount);

		TransferResultDto transferResultDto = transferService.transfer("1", "2", "田中太郎", 1000);
		TransferResultDtoMock actual = new TransferResultDtoMock(transferResultDto);
		assertThat(actual, is(expected));
	}


	@EqualsAndHashCode(callSuper = true)
	private class TransferResultDtoMock extends TransferResultDto {
		public TransferResultDtoMock(String payerAccountId,
									 String payeeAccountId,
									 String payerName,
									 String payeeName,
									 long transferAmount,
									 long amount) {

			super(payerAccountId, payeeAccountId, payerName,
					payeeName, transferAmount, amount);

		}

		public TransferResultDtoMock(TransferResultDto transferResultDto) {
			super(transferResultDto.getPayerAccountId(),
				  transferResultDto.getPayeeAccountId(),
				  transferResultDto.getPayerName(),
				  transferResultDto.getPayeeName(),
				  transferResultDto.getTransferAmount(),
				  transferResultDto.getAmount());
		}
				

	}
}
```

## テストコードを書くにあたって意識すべきこと
### ビジネスロジックに関する部分は独立した実装にする

最初にHelloJunit内でGreetDaoをDIするロジックを、アノテーションからコンストラクタ型に変更したと思います。
その理由としては、アノテーションで表現する場合、HelloJunitがフレームワーク（Seasar2）に**<font color="Red">依存する</font>**形になってしまうからです。

```java:プロダクトコード
public class HelloJunit {

	GreetDao greetDao;
	
	public HelloJunit(GreetDao greetDao) {
		this.greetDao = greetDao;
	}

	public String sayGreeting() {
		return greetDao.getGreet();
	}
}
```

例えば今後Springに移行することになった場合、アノテーションでDIする場合は @ Autowiredを利用することになります。
なので、コンストラクタでDIすることでフレームワークへの変更も用意になります。

また、テストコードという観点からもアノテーションで実現している場合には、クラスの宣言時に`@RunWith(Seasar2.class)`を指定する必要がありますが、コンストラクタでDIしている場合には、new してあげればテストが簡単にできます。

#### 参考記事
[実践クリーンアーキテクチャ](https://nrslib.com/clean-architecture-with-java/)


### テストが書きやすいコードを書く

テストを行うにあたり、テストが書きやすいコードというのは重要になります。

例えば以下のようなコードは、`TimeJunit.getTime()`staticメソッドの結果に応じて条件分岐をしているのですが、staticメソッドはモックで置き換えることが難しいです。

```java:staticメソッドを利用している
public class ExampleJunit {
	public String sayGreeting() {
		int term = TimeJunit.getTime();
		switch(term) {
		case 1:
			return "パスタ";
		case 2:
			return "ラーメン";
		default:
			return "ご飯";
		}
	}
}
```

あとは継承していてスーパークラスのメソッドを利用している場合もテストしにくいコードになります。
例えば古いシステムだとよく使われているようなこんなロジック。

```java:スーパークラス
public abstract class AbstractJunit {
	public String sayGreeting() {
		return this.getMessage();
	}
	abstract protected String getMessage();
}
```

```java:継承クラス
public class ExampleJunit extends AbstractJunit {
	protected String getMessage() {
		int term = 1;
		switch(term) {
		case 1:
			return "パスタ";
		case 2:
			return "ラーメン";
		default:
			return "ご飯";
		}
	}
}
```

今回の例であればテスト可能ですが、実際に業務で継承を利用しているようなケースだと、スーパークラスが神様クラス（なんでもロジックを入れる便利クラス）になっていて、スパゲッティソースになっていたりするので、テストでリフレクションとかしなきゃいけなくなってしまうので扱い辛いです。

本当に継承を使うべきか考えて実装しましょう。

#### 参考記事
[テストが書きやすいコードは良いコード](https://qiita.com/jnchito/items/a7d3005aaa9401480a47)

## その他参考

http://s2container.seasar.org/2.4/ja/easyMock.html