LoginSignup
8
12

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-21

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

実行環境

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

モックとは

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

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

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

HelloJunit
public class HelloJunit {

    @Binding
    GreetDao greetDao;

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

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

どうやってモックを作るのか

以下のような方法があります。

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

モックライブラリとしては、MockitoJMockit、そしてEasyMockMockInterceptorなどがあります。

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

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

EasyMockを利用したテストコードサンプル

とりあえずコード

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

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

    GreetDao greetDao;

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

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

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

テストコード
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メソッドが想定通りの呼び出され方をしているか確認します。

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

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

テストコード
@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.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の戻り値によって分岐を入れてみました。
今回のテストでは、パスタが返却されるパターンをテストしたいと思います。

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

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

テストコード
@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型のメソッドをテストする方法です。

プロダクションコード(GreetDao)
public interface GreetDao {
    public void insertGreet(String greet);
}
プロダクションコード(HelloJunit)
public String addGreet() {
    greetDao.insertGreet("Good Night");
    return "OK";
}

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

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

テストコード
@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を継承してテストします。

テスト対象のプロダクションコード
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);
    }
}
戻り値の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を利用して自動生成しました

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)に依存する形になってしまうからです。

プロダクトコード
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 してあげればテストが簡単にできます。

参考記事

実践クリーンアーキテクチャ

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

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

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

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

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

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

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

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

参考記事

テストが書きやすいコードは良いコード

その他参考

8
12
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
8
12