(JAVA) Springで同じサービス内のメソッドをモック化する

(JAVA) Springで同じサービス内のメソッドをモック化する

2024年5月30日

一般的にSpringはレイヤードアーキテクチャを採用しており、サービスで主にビジネスロジックを処理します。

サービスのコードが増えれば増えるほど、似たコードが多くなることが一般的です。このような場合、共通ロジックを処理する別のレイヤーを設けて分離するケースもありますが、通常はサービス内に共通メソッドを宣言して処理することが多いでしょう。

例えば、次のようにします。

@Service
public class SomeService {
    public void methodA() {
        // do something
    }

    public void methodB() {
        methodA();
        // do something
    }
    
    public void methodC() {
        methodA();
        // do something
    }
}

上記のコードでは、methodAmethodBmethodCで共通に使用されるメソッドです。

もしmethodB()のテストコードを書く必要がある場合、methodA()に対するテストコードも書かなければならないでしょう。最悪の場合、「共通」 ロジックであるため、methodC()をテストする際にもmethodA()のロジックを一緒にテストしなければならないことが発生するかもしれません。

重複を減らそうと思ったのに、かえってテストコードで重複が増える状況が発生する可能性があります。

モック化

もし他のクラスのオブジェクト内のメソッドを使用する場合、モック化という方法でメソッドの模擬動作を指定し、テストコードを書きます。(モック化されたメソッドは既にテストされたと仮定するため、そのメソッド内でのテストは不要であるからです。)

しかし、モック化も万能ではなく、同一クラスでの場合、話が少し異なります。

一般的にJavaはモック化のためにプロキシオブジェクトを生成し、そのプロキシオブジェクトをあたかも実際のオブジェクトのように使用しますが、同一クラスを模擬オブジェクトおよび実際のオブジェクトとして使用することは困難です。

一般的にJavaを除く他の言語では同一クラス内のメソッドをテストすることは前述のように重複が発生する可能性があります。

Pythonのような場合、この状況でモンキーパッチを利用し同一クラス内のメソッド自体のポインタを変更してテストを行ったり、

以前Goで開発していた時には、そのような共通のメソッドポインタを別途宣言し、テストの時点でそのメソッドポインタをテスト時に注入するか、前述のように共通のロジックを処理するレイヤーを別に分離してテストする方法を使いました。(このようにインターフェイスが異なるとモック化はどの言語でも難しくありません。)

Javaではこのような場合、@Spyを使用して同じクラス内のメソッドをモック化できます。

@Spy

image

まず、アノテーションについて知る前にSpyオブジェクトについて知りましょう。

スパイの意味を考えてみても良いのですが、映画でスパイを考えたらどうでしょうか?

実際は味方であるかのように振る舞いながら、特定の状況では別の行動を取ることを考えると理解が簡単でしょう。

Spyオブジェクトはこのようにある程度のケースでは実際のオブジェクトのメソッドを呼び出し、あるケースでは特定のメソッドの動作を変更できるオブジェクトです。

@SpyはこのようなSpyオブジェクトを生成するアノテーションです。

では、上のコードを@Spyアノテーションを利用してテストしてみましょう。

モック依存性が別途ある場合、通常モック化するオブジェクトに@Mockアノテーションを、依存性を受けるオブジェクト(テスト対象メソッドが存在するオブジェクト)に@InjectMocksアノテーションを使用します。

注意すべき点はこのように@Spyアノテーションを共に活用する場合、@Spyアノテーションが@InjectMocksアノテーションより先に宣言されるべきということです。

@Service
public class SomeService {
    @Mock
    SomeDependency someDependency;
    
    @Spy
    @InjectMocks
    SomeService someService;
}

ではmethodA()をテストするコードを書いてみましょう。

    @Test
    void testMethodA() {
        // given
        doNothing().when(someService).methodA();
        
        // when
        someService.methodA();
        
        // then
        verify(someService, times(1)).methodA();
    }

このように@Spyアノテーションを活用すれば、同じクラス内のメソッドでもモック化することができます。

共通ロジックレイヤー分離時のテストコード

ただし、このように同じクラス内のメソッドをモック化することはテストコードの可読性を低下させる可能性があるため、個人的な考えとしてはこのような場合には共通ロジックを処理する別のレイヤーを設けるのが良いと思います。

例えば、methodA()を別のクラス(ここではSomeServiceSupportと命名)に分離して処理し、SomeServiceではそのクラスを注入する方法があります。

SomeServiceSupport.java
@Component
public class SomeServiceSupport {
    public void methodA() {
        // do something
    }
}
SomeService.java
@Service
@RequiredArgsConstructor
public class SomeService {

    private final SomeServiceSupport someServiceSupport;
    
    public void methodB() {
        someServiceSupport.methodA();
        
        // do something
    }
    
    public void methodC() {
        someServiceSupport.methodA();
        
        // do something
    }
}

このように分離すれば、同じクラス内のメソッドをモック化する必要がなくなるため、@Spyアノテーションを通じたテスト、メソッド間の複雑な相関関係を減らすことができるでしょう。

    @SpringBootTest
    class SomeServiceTest {
        
            @Mock
            SomeServiceSupport someServiceSupport;
            
            @InjectMocks
            SomeService someService;
            
            @Test
            void testMethodB() {
                // given
                doNothing().when(someServiceSupport).methodA();
                
                // when
                someService.methodB();
                
                // then
                verify(someServiceSupport, times(1)).methodA();
            }
            
            @Test
            void testMethodC() {
                // given
                doNothing().when(someServiceSupport).methodA();
                
                // when
                someService.methodC();
                
                // then
                verify(someServiceSupport, times(1)).methodA();
            }
    }

このコードでは実際のところ、テストコード自体はあまり変わりませんでした。しかし、階層間の分離によって、他の開発者がこのレイヤーが共通ロジックを処理するレイヤーであることが分かるため、メンテナンスにおいては多少の利点があるように思います。