モックを使用した「Test.isRunningTest()」からの卒業

今回は、モックを使用して本番コードをクリーンに保つ方法をご紹介します。

はじめに

こんにちは、テラスカイのS.Kです!

皆様は、これまでの案件作業の中でモックを実装したことはありますでしょうか?
筆者は、モックの存在は知っていましたが、いざ案件で実装してみると、とても便利な手法だなと感じました。

そこで今回は、本番コードとテストコードをクリーンに保つことができるモックの使用方法をご紹介したいと思います。

そもそもモックとは?

モックは一言で言うと「結果を制御できる接続先」です。
本来の接続先(外部API連携など)のふりをして、あらかじめ定めておいたレスポンスを返してくれます。
なぜ、モックが必要なのかは以下の理由が挙げられます。

◆ 外部依存を避ける
例えば「外部APIが未完成だったら」「相手のサーバーがダウンしていたら」これらの影響を直接受けると、外部連携のテストが実施できなくなる可能性が生じます。

◆ 状況の再現
レスポンスの結果をテストしたいとき、特に異常系ではサーバー側をエラーにしなければなりません。
テストのために、本物のサーバーをエラー状態にさせるわけにはいきませんよね...

結論、外部の状況に左右されずに、構築したロジックが問題なく動作するかを確認するでうえ重要な役割を果たしてくれているのがモックになります!

そんな中、モックの役割は前述した「結果を制御できる接続先」にとどまりません。
実は、「本番コードにおける”妥協”をなくし、クリーンに保つ」ための強力な武器になりえるのです!

本番コードに紛れた「妥協」

なぜモックが「クリーンに保つ武器」になるのか。
筆者が実際に案件で見かけた内容をもとに、深堀していきたいと思います。

➀ プロジェクトで目にしたことはありませんか?

皆様は、テストコードを書いていて「どうしても値が固定できない...!」と思ったことはありませんか?
そんな悩みを解決してくれるのが「Test.isRunningTest() 」です。

例えば、「今日が申込期限かどうか」を判定する、以下のロジックがあると仮定します。
public class ApplicationLogic {
        public void checkDeadLine(){
                Date today;

                if (Test.isRunningTest()){
                        today = Date.newInstance(2026, 4, 1);
                } else {
                        today = Date.today(); 
                }

                if (today < Date.newInstance(2026, 5, 1)) {
                         // 期限内処理
                }
        }
}
example01
日付(today)が2026年5月1日以前の場合は期限内の処理を行うとしていますが、todayは「Date.today」を設定しています。

そのため、テストを実施する日付によって「1ヶ月前は問題なくテストがパスしていたのに...」という問題が発生します。
時間が原因となると、どうしようもないですよね...

そこで、「Test.isRunningTest() 」を用いてテスト実施時のみの日付を設定することで、この問題を解決することができます。

➁”妥協”があることへのデメリット

では、なぜTest.isRunningTest() の実装が”妥協”となってしまうのでしょうか。
主に以下の理由が挙げられます。

◆ 本番ロジックがテストされないリスク
Test.isRunningTest() を実装後、テストコードが検証しているのは「テスト専用の処理」だけになります。
肝心な本番で動くコードは一度も実行されないまま、本番環境へデプロイされる可能性があり、「テストは問題なかったが、本番のみエラーが発生する」という事象が起こり得ます。

◆ 本番コードの可読性
「テスト用のコード」は本番環境では実行されることはありません。
また、Test.isRunningTest() で使用しているデータに仕様変更が生じた場合、本番コードとテスト用コードの両方に修正が発生するため、メンテナンス性の低下を引き起こす可能性があります。

では、どのような実装をすることで本番コードにおける”妥協”をなくすことができるのか。
サンプルを使用しながらご紹介したいと思います!

妥協をなくし、本番ロジックをクリーンに保つ

実装を行うにあたり、まず初めに大事になるのが、「本番コードに書かれている処理を、1つの独立したロジックとして切り出す」ことです。
前準備として大事な工程になるため、詳しく見ていきます。

➀【前準備】ロジックを切り出す、Interfaceの実装

ロジックを切り出すために、「Interface」を使用します。
Interfaceの役割はロジックを外部から差し替え可能な状態にすることです。

具体的なコード例は「本番コードに紛れた「妥協」_➀」でご紹介した申込期限を判定するロジックを用いてご説明します。
public interface DateLogicInterface {
    Date getTargetDate();
}
example02
これにより、使用するクラスごとにメソッド内の定義を定めることができます。

次に、Interfaceで定義したメソッドを使用するApexクラスを作成します。
ポイントは、「作成したInterfaceを実装することで、共通のメソッドが使用可能になること」です。
// 本番コードでの使用 if(Test.isRunningTest())は使用しない!!
public class RealLogic implements DateLogicInterface {
    public Date getTargetDate() {
        return Date.today(); // 本番は今日の日付を返す
    }
}
example03
// 本番コード
public class ApplicationLogic {
        @TestVisible
        private static DateLogicInterface logic = new RealLogic();

        public void checkDeadLine(){
                Date today = logic.getTargetDate(); // 切り出したロジックから日付を取得!

                if (today < Date.newInstance(2026, 5, 1)) {
                         // 期限内処理
                }
        }
}
example04
このような実装により、Test.isRunningTest()を実装することなくクリーンな本番コードを実現することができます。

最後に、Interfaceを実装したテストコード用のApexクラスを作成...
と言いたいところですが、実は「テスト用のInterface実装クラス」は新しく作成する必要はありません!

Salesforceが提供している「Stub API(System.StubProvider)」を用いることで、テストコード内で動的に「モック」を生成し対応することができます。

➁【実践】StubAPIでモックを動的に生成

StubAPIの実装は、「モック用のデータ定義」と「モックの作成」の2ステップで行うことができます。

◆ モック用のデータ定義
テストコード内に「該当するメソッドが呼ばれたら、特定の値を返す」というルールを定義します。
// テストクラスに定義
private class ApplicationLogicMock implements System.StubProvider {
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
                                   Type returnType, List<Type> listOfParamTypes, 
                                   List<String> listOfParamNames, List<Object> listOfArgs) {
        
        // 「getTargetDate」が呼ばれたら、テストに都合の良い日付を返す!
        if (stubbedMethodName == 'getTargetDate') {
            return Date.newInstance(2026, 4, 1);
        }
        return null;
    }
}
example05
この実装により、呼ばれたメソッド名(stubbedMethodName)を判定し自由に結果を設定できます。

◆ モックの作成
テストコード内に定義したルールを用いてインスタンスを生成し、本番クラスのInterface変数に設定します。
@isTest
static void testApplicationLogic () {
    // 1. モックを用意
    ApplicationLogicMock mock = new ApplicationLogicMock();

    // 2. Stub APIを使って「モック」を生成
    DateLogicInterface mockInterface = (DateLogicInterface)Test.createStub(DateLogicInterface.class, mock);

    // 3. ★ 本番クラスに、モックを差し込む
    ApplicationLogic.logic = mockInterface;

    // 4. 実行
    Test.startTest();
    new ApplicationLogic().checkDeadLine();
    Test.stopTest();

    // アサーションで結果を確認...
}
example06
この実装により、テストクラス側から本番コードのロジックを検証できるようになります。

以上の【前準備】と【実装】の手順を踏むことで本番コードに「Test.isRunningTest()」を実装することなく、テスト側から検証結果のコントロールが可能となりました!

まとめ

今回は、Test.isRunningTest() という妥協に焦点を当て、本番コードをクリーンに保つ方法をご紹介しました。

改めて紹介した内容で実装を行うことで、「本番コードの純度を高める」「テストが外的な要因に左右されず落ちにくくなる」といった結果を望めるようになります。

確かに、Test.isRunningTest() はお助け機能であり、使用することに問題があるわけではありませんが、コードの品質を高めていくうえで、大切な観点になるのではないでしょうか?

最後までご覧いただきありがとうございました。皆様の参考になれば幸いです!

参考