R e i - D r e a m
for Laravel
Guest
login

最終投稿日:2026年09月27日

モック化

ユニットテスト最大の障壁

単体テストで一番の障壁は、テストに直接関係ない(または常に一定の値としたい)オブジェクトの振舞いです。
例えばログオブジェクトや、DB取得オブジェクト等です。
DBから取得した値が『hoge』なら《true》を返却するメソッドのテストに対してDBにテストレコードを入れるのは面倒ですよね。
そこでテストとは無関係な情報をダミーデータに置き換える必要がでてきます。
これが『モック化』です。
冒頭のサンプルソースを使いモック化を説明します。
src/test/java/service/SampleServiceTest.java

package service;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class SampleServiceTest {
    private SampleService target = new SampleService();
    @Test
    void test_01() {
        assertEquals(target.chkUserInfo("hoge"), "ユーザー『hoge』は存在します。");
    }
}

○ 対象メソッドの処理内容
引数の内容を元にユーザー情報を検索し、ユーザーの状態によりメッセージを返却する。
○ テストの趣旨
仮にDB検索結果に《name = hoge》が存在する場合【ユーザー『hoge』は存在します。】が返却されるかの確認。
○ テストクラスの内容
テスト対象クラス『SampleService』をインスタンス名『target』として初期化する。
テストケースとして、引数《hoge》を関数《chkUserInfo》に渡した際の返却結果を確認。
○ ユニットテスト実行
それでは実行してみましょう。
    ※テスト終了後は『JUnit』タブを選択します
結果は、テスト失敗と言うよりか『ユニットテスト処理自体のエラー』となります。
障害トレースを見てみます。
障害トレース

java.lang.NullPointerException: Cannot invoke "dto.SampleDto.setName(String)" because "this.dto" is null
    at service.SampleService.chkUserInfo(SampleService.java:24)
    at service.SampleServiceTest.test_01(SampleServiceTest.java:10)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

要するに以下の内容で怒られています。
    対象クラスの[24行目]で『dto.setName()』って関数あるけど、DTO自体が《null》だぞ!

private修飾子を無視してモック化する

ユニットテストでは『@Inject』されたクラスのインスタンスは《null》となります。
またフィールドプロパティは『private』修飾子を使いがちですよね。
当然テストクラスは、テスト対象クラスとは縁もゆかりもないのでアクセスできません。
そこで、Reflection(リフレクション)機能を使いモック化します。
先ほどのエラーとなった『SampleServiceTest』の内容を以下の様に書き換えます。
SampleServiceTest.java

package service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dto.SampleDto;
import java.lang.reflect.Field;

class SampleServiceTest {

    private SampleService target;
    // モック化
    SampleDto mockSampleDto = mock(SampleDto.class);
    @BeforeEach
    void setUp() throws Exception {
        target = new SampleService();
        Field field = SampleService.class.getDeclaredField("dto");
        field.setAccessible(true);
        field.set(target, mockSampleDto);
    }
    @Test
    void test_01() {
        assertEquals(target.chkUserInfo("hoge"), "ユーザー『hoge』は存在します。");
    }
}

この状態でテストを再実行してみてください。
『JUnit』タブを確認すると【赤帯】となりますが、処理エラーの内容が変わっています。
    java.lang.NullPointerException: Cannot invoke "logic.SampleLogic.selectUserInfo(dto.SampleDto, String)" because "this.appLogic" is null
つまり最初のエラーは解消され、次のエラーに到達したって事ですw
ユニットテストは下手をすると、この『ヌルポ地獄』で沼に嵌るケースが多いので慎重に設定する必要があります。
それでは、最初の『DTO』エラーが何故解消したかを説明します。
① SampleDto mockSampleDto = mock(SampleDto.class);
まずは当然モックを作成します。
② Field field = SampleService.class.getDeclaredField("dto");
『SampleService』クラス定義の中から、インスタンス名『dto』の情報を検索し取得する
③ field.setAccessible(true);
『true』とする事で『private』修飾子を無視できます
④ field.set(target, mockSampleDto);
テスト対象インスタンス『target』に対し、定義したモック『mockIndexDto』を強制的に代入します
上記②③④を関数『setUp()』で設定します。

モック化のお手軽設定

複数のインジェクトされたインスタンスに対し、先ほどの章の様に②③④を繰り返し設定するのはコードが冗長となります。
そこで以下の様にコードを変えてみましょう。
SampleServiceTest.java

package service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dto.SampleDto;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

class SampleServiceTest {

    private Field findFieldInHierarchy(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        Class<?> current = clazz;
        while (current != null) {
            try {
                return current.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                current = current.getSuperclass();
            }
        }
        throw new NoSuchFieldException(fieldName + " が,プロパティフィールドから見つかりません。");
    }
    private SampleService target;
    // モック化
    SampleDto mockSampleDto = mock(SampleDto.class);
    @BeforeEach
    void setUp() throws Exception {

        target = new SampleService();
        Map<String, Object> fields = new HashMap<>();
        fields.put("dto", mockSampleDto);
        for (Map.Entry<String, Object> entry : fields.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            Field field = findFieldInHierarchy(target.getClass(), key);
            field.setAccessible(true);
            field.set(target, value);
        }
    }
    @Test
    void test_01() {
        assertEquals(target.chkUserInfo("hoge"), "ユーザー『hoge』は存在します。");
    }
}

『長っ!』と思っていませんか?
でもこれ、案外便利なんですよ。
例えば先ほどの章の『because "this.appLogic" is null』でエラーとなっていた《appLogic》をモック化する場合は、以下を追記するだけです。
① モックインスタンスの追加
SampleLogic mockAppLogic = mock(SampleLogic.class);
② Mapインスタンス『fields』に新たなモックを追加
fields.put("appLogic", mockAppLogic);
①は、どんな仕組みにしようと常に定義する内容です。
ですので実質②の1行を設定するだけでモックインスタンスをテスト対象クラスに設定ができます。
それではこの状態でテストを実行してみましょう。
テスト結果

org.opentest4j.AssertionFailedError: expected: <ユーザーは誰もいません。> but was: <ユーザー『hoge』は存在します。>
    at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
    at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
    at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
    at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
    at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
    at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1145)
    at service.SampleServiceTest.test_01(SampleServiceTest.java:47)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

おめでとうございます。
無事ユニットテストの障害が解消され、本来のテスト失敗である《赤帯》となりましたw

モックインスタンスの返却値を制御する

先ほどの章でユニットテストは《失敗》という結果で通せました。
では何が原因で失敗したかを確認しましょう。
SampleService.java

public String chkUserInfo(String name) {
    logger.info("------------- SampleService.chkUserInfo ---------------");
    dto.setName(name);
    String ymd = Da<teUtil.getNowToString();

    List<User> list = appLogic.selectUserInfo(dto, ymd);
    if (list != null && list.size() > 0) {
        for (User ent : list) {
            if (ent.getName().equals(name)) {
                return "ユーザー『" + name + "』は存在します。";
            }
        }
        return "ユーザー『" + name + "』はいません。";
    }
    return "ユーザーは誰もいません。";
}

上記ソースは、テスト対象である『SampleService』クラスの抜粋です。
原因はインスタンス『list』の値となります。
『list』はモック化した『appLogic』からの取得となり、モックした以上は当然《null》が返却されます。
あとはコードの通り『ユーザーは誰もいません。』が返却される流れです。
ここで重要なのが『このクラスのテスト観点は何だっけ?』を理解する事です。
テスト観点は以下となります。
    受取った『list』インスタンスの中身をループ処理した結果、引数と同様の《name》が存在するケースの返却内容を確認する。
つまりモック化した『appLogic.selectUserInfo(dto, ymd)』のテストではありませんので、任意の値を代入してもテストの品質に影響はありません。
それではテストソースを修正して、モックに返却値を設定します。
SampleServiceTest.java

@Test
void test_01() {
    String name = "hoge";
    doAnswer(invocation -> {
        List<User> list = new ArrayList<>();
        User ent = new User();
        ent.setName(name);
        list.add(ent);
        return list;
    }).when(mockAppLogic).selectUserInfo(any(), any());
    
    assertEquals(target.chkUserInfo(name), "ユーザー『hoge』は存在します。");
}

上記コードは『SampleServiceTest』クラスのテスト部分のメソッドとなります。
詳細は後述いたしますが、モック化した特定のメソッドに対して任意の値を返却する事ができます。
上記の内容でユニットテストを実行すると、今度は《緑帯》となり成功します!
ポイント

上記コードの『any()』は、いわゆるワイルドカード的なメソッドです。
今回はメソッド『selectUserInfo』に対応する返却値を設定しましたが、テスト中に複数このメソッドが記述されている場合はどうなるでしょう?

答えは、全て同じ値が返却されます。
詳細は後述しますが、それでは困るテストケースの場合は、引数の内容を厳密に記述する等対策する必要があります。

テストインスタンスのモック化(SPY)

特殊なテストケースとして、少しテスト対象クラスを修正します。
SampleService.java

package service;

import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utility.DateUtil;
import dto.SampleDto;
import entity.User;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;

@Dependent
public class SampleService {
    private static final Logger logger = LoggerFactory.getLogger(SampleService.class);
    @Inject
    private SampleDto dto;
    public String chkUserInfo(String name) {
        logger.info("------------- SampleService.chkUserInfo ---------------");
        dto.setName(name);
        String ymd = DateUtil.getNowToString();

        List<User> list = this.selectUserInfo(dto, ymd);
        if (list != null && list.size() > 0) {
            for (User ent : list) {
                if (ent.getName().equals(name)) {
                    return "ユーザー『" + name + "』は存在します。";
                }
            }
            return "ユーザー『" + name + "』はいません。";
        }
        return "ユーザーは誰もいません。";
    }

    
    List<User> selectUserInfo(SampleDto dto, String ymd) {
     return null;
    }
}

『SampleLogic appLogic』を削除して、メソッド『selectUserInfo』をテスト対象クラス内の別メソッドとしました。
ただしこのメソッドはまだ途中で、現状は《null》しか返却しません。
別に途中である必要もありませんが、メソッド『selectUserInfo』の内容が複雑な場合は、テスト観点でもない関数内のコードの返却値を複数セットするのは非効率です。
この状態の場合、何が厄介かと言うと『テスト対象クラスをモック化はできない』事となります。
しかしこのままだと、メソッド『selectUserInfo』は《null》を返却するためテストは確実に《赤帯》となります。
この場合の解決策は、テスト対象クラスに【SPY(スパイ)】を忍び込ませるのです。
以下テストクラスのサンプルとなります。
SampleServiceTest.java

package service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import dto.SampleDto;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

class SampleServiceTest {
    private Field findFieldInHierarchy(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        Class<?> current = clazz;
        while (current != null) {
            try {
                return current.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                current = current.getSuperclass();
            }
        }
        throw new NoSuchFieldException(fieldName + " が,プロパティフィールドから見つかりません。");
    }

    private SampleService target;
    private SampleService spyTarget = new SampleService();
    // モック化
    SampleDto mockSampleDto = mock(SampleDto.class);
    @BeforeEach
    void setUp() throws Exception {

        target = Mockito.spy(spyTarget);
        Map<String, Object> fields = new HashMap<>();
        fields.put("dto", mockSampleDto);
        for (Map.Entry<String, Object> entry : fields.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            Field field = findFieldInHierarchy(target.getClass(), key);
            field.setAccessible(true);
            field.set(target, value);
        }
    }
    @Test
    void test_01() {
        String name = "hoge";
        assertEquals(target.chkUserInfo(name), "ユーザー『hoge』は存在します。");
    }
}

ポイント

実はSPYを代入するタイミングはとても重要です。
感覚としてはスパイだけに『誰もいない時に忍び込ませる』です。

要するに一番最初となり、最後にSPYを代入すると今まで『mockDao』『appLogic』等を設定してたのがリセットされてしまいます。

この状態でテストを実施すると《赤帯》となり、テストは失敗します。
理由は単純で『スパイは忍び込んだが、まだ仕事をしていない』からとなります。
それではスパイに仕事をしてもらいます。
SampleServiceTest.java

@Test
void test_01() {
    String name = "hoge";
    doAnswer(invocation -> {
        List<User> list = new ArrayList<>();
        User ent = new User();
        ent.setName(name);
        list.add(ent);
        return list;
    }).when(target).selectUserInfo(any(), any());
    
    assertEquals(target.chkUserInfo(name), "ユーザー『hoge』は存在します。");
}

テストケースメソッドの内容となりますが、何処かで見た内容ですね。
そう《appLogic》をモック化してた時とほぼ同じ設定となります。
違いは『when』の引数の内容が【mockAppLogic】から【target】に変更しています。
この様に、SPYを忍ばせる事でテスト対象クラスのインスタンスである『target』を扱える様になります。
ポイント

今回『SPY』を仕込んだメソッド『selectUserInfo』があるクラス『SampleService』に注目して下さい。
実は対象メソッドに『public』や『private』の修飾子を付与していません。
この状態を『パッケージプライベート』と呼び、同じパッケージ内からしかアクセスができません。

仮にメソッド『selectUserInfo』が自クラスからしか呼ばれないから『private』とした場合『SPY』は仕込めません。
ここが『ユニットテストと実装のジレンマ』となります。
つまり見方によっては『テストの為に堅牢性を捨ててるのでは?』と思われます。
しかし他クラスからコールする場合は当然『public』を使います。
これって堅牢性を捨ててますか?
当然必要だから『public』にしている為それには当たりません。
でも本来『クラスA』からしかコールしない設計でも『public』にすると『クラスB』からも呼べてしまいますよね?
ここで『う~ん、困った』なんて言うレビュアーはいません。
そのためのレビューで、仕様通り対象クラスからのみコールされていればレビュー合格です。
今回の事例も同じで単純に『ユニットテストで必要だから「パッケージプライベート」にしたのか。よし合格!』となるだけです。
たまにプロジェクトで、設計は嬉々として指摘するのに製造のレビューに関しては無言で合格にするレビュアーがいます。
要するに何も見ていません(理解していません)。
仮にパッケージプライベートにした事で障害が発生した場合、そのプロジェクトは『public』『private』に関わらず障害を発生させるでしょう。

『static』クラスのモック化

実は『static』クラスの場合は、少し勝手が変わってきます。
実は今回のテスト対象クラスである『SampleService』クラス内にも『static』クラスから値を取得しているコードがあります。
    String ymd = DateUtil.getNowToString();
このクラスの内容は以下となります。
src/main/java/utility/DateUtil.java

package utility;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class DateUtil {
    public static String getNowToString() {
        LocalDate date = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
         return date.format(formatter);
    }
}

単純に現在日付を『yyyyMMdd』形式として文字列で返却するメソッドとなります。
システムでは良くある仕組みですよね。
ただしこの《日付》がテストの重要なキーだった場合はどうでしょう?
ユニットテストを実施する日によって、テスト内容が変わってしまうと品質を担保できません。
従って、この静的クラスをモック化する必要が生まれてきます。
○ モック化する方法
MockedStatic mockDateUtil = mockStatic(DateUtil.class);
○ 値の返却設定
mockDateUtil.when(DateUtil::getNowToString).thenReturn("20260101");
これだけです。 最後にテスト対象メソッド側のコードを以下の様に変更してテストを実施してみましょう。
return "ユーザー『" + name + "』は存在します。";
⇒
return "ユーザー『" + name + "』は存在します(" + ymd + ")";
設定通り『20260101』と表示されるハズです。
ポイント

実は『static』クラスのモックインスタンスには特殊なルールが存在します。
それは『テスト単位でモックインスタンスを閉じる事』です。

試しに同じ内容で良いので2つ目のテストケース『test_02』関数を作成してテストを実施して下さい。
実行後各テストメソッドのアイコンをクリックする事で『障害トレース』の内容が切り替わりますが、2回目のテスト結果は明らかにユニットテストのエラーとなっています。
これは1度目のモックインスタンスを閉じていない事に起因するエラーとなります。
解決方法は簡単で各テストメソッドの終了時にモックインスタンスを閉じれば良いです。
各テストケースの終了時に呼ばれるアノテーション『@AfterEach』を使いましょう。
@AfterEach
void afterEach() throws Exception {
    if (mockDateUtil != null) {
        mockDateUtil.close();
    }
}

カバレッジからテスト結果を確認する

カバレッジとは、ユニットテストを実施した際に『どのコードを実行したか』を視覚的に確認する方法です。
テストをする際の『実行(R)』の近くに『カバレッジ(V)』がありますので、そのメニューからユニットテストを実行します。
カバレッジが非表示の場合

[ヘルプ] > [Eclipseマーケットプレース] > 検索窓『EclEmma』で検索
    ・EclEmma Java Code Coverage

上記が対象のプラグインなので、インストール有無を確認します。
『インストール済み』となっている場合は、ボタンを押下します。
『更新』ボタンとなるので押下します。
『完了』を押下します。
エクリプスから再起動を求められますので、再起動します。

では『カバレッジ > JUinitテスト』から実行してみましょう。
『テストクラス』『テスト対象クラス』共にソース内がカラフルになります。
緑が実際に処理が通過したコードとなります。
『カバレッジ』タブから、テスト対象クラスである『SampleService.java』を開いてみましょう。
カバレッジが【90%】前後になっていいれば、そのユニットテストは【OK】を貰えるレベルと言えるでしょう。
レンズモード 【新規購入限定用】
ログインしてコメントを残そう!!


きっぷる