【RepositoryTest】引数のSideEffectのテスト実装

【RepositoryTest】引数のSideEffectのテスト実装

SideEffectとは

SideEffect(副作用)とは、

プログラミングにおける副作用(ふくさよう)とは、ある機能がコンピュータの(論理的な)状態を変化させ、それ以降で得られる結果に影響を与えることをいう。代表的な例は変数への値の代入である。

wikipedia

今回の例だと上記の説明とは微妙に違うのですが、
メソッドの引数に入れたオブジェクトが、そのメソッド実行後にオブジェクト内の値が変化することを指しています。

具体的に説明していきます。

RepositoryによるSideEffectの例

Repository層において、データをINSERTする下記のメソッドがあります。

 int create(Address address);

AddressテーブルにaddressオブジェクトのデータをINSERTするメソッドで、
戻り値はINSERTに成功した件数が返ってきます。

そして、Addressテーブルの主キーに相当するaddressId以外をINSERTし、
addressIdはDBのシーケンス機能から取得され、メソッド実行後に引数のaddressオブジェクトのaddressIdに設定されます。

  1. Addressオブジェクトを生成する(addressIdだけ設定していない)
  2. createメソッドの引数にAddressオブジェクトを渡す
  3. メソッドの実行結果が戻り値として返ってくる
  4. AddressオブジェクトのaddresIdにDBのシーケンス機能から取得した値が格納される

テスト

このRepositoryメソッドの実装が下記だとして、

@Repository
@RequiredArgsConstructor
public class AddressRepositoryImpl implements AddressRepository {

    private final AddressMapper addressMapper;

    public int create(Address address) {
        return addressMapper.create(address);
    }
}

DBに直接アクセスしているMapperをMock化し、
Repositoryの単体テストを行うとするとまずこうなります。

テストケース1

 @Test
 void createTest() {

        //GIVEN
        Address address = Address.builder()
                .address("Tokyo")
                .district("")
                .city(City.builder().cityId(90).build())
                .phone("000-1111-2222")
                .lastUpdate(Timestamp.valueOf(LocalDateTime.of(2019, 9, 9, 12, 12)))
                .build();

        when(addressMapper.create(any())).thenReturn(1);

        //WHEN
        int actual = addressRepository.create(address);

        //THEN
        assertThat(actual).isEqualTo(1);
 }

MapperのMockで正常にINSERTされたという件数を返してそれを検証していますが、
SideEffectによる影響は検証できていません。
MockでSideEffectを再現する方法は次のテストケースのようになります。

テストケース2

  @Test
  void createSideEffectTest() {

        //GIVEN
        Address address = Address.builder()
                .address("Tokyo")
                .district("")
                .city(City.builder().cityId(90).build())
                .phone("000-1111-2222")
                .lastUpdate(Timestamp.valueOf(LocalDateTime.of(2019, 9, 9, 12, 12)))
                .build();

        when(addressMapper.create(any())).then(firstArgumentsSideEffect());

        //WHEN
        assertThat(address.getAddressId()).isEqualTo(0);
        int actual = addressRepository.create(address);

        //THEN
        assertThat(actual).isEqualTo(1);
        assertThat(address.getAddressId()).isEqualTo(1110);
  }

  private Answer<Integer> firstArgumentsSideEffect() {
        return invocation -> {
            Object[] args = invocation.getArguments();
            Address arg = (Address) args[0];
            arg.setAddressId(1110);
            return 1;
        };
  }

mock時にthenReturnではなくthenを使用し、
内部でAddressオブジェクトを操作しています。
returnで1を返しているのがメソッドの戻り値に相当します。
引数が二つある場合はargs[1]を操作します。

まとめ

SideEffectはコードが複雑になり、可読性が悪くなるのでやめたほうがいいのですが、
DBのシーケンス機能から生成された値を使用するというような時は使わざるを得ません。(少なくともmybatisの場合は使わざるを得ない)

あと、「副作用」じゃなくて「SideEffect」って言うのがスタイリッシュでかっこいいです。
是非使っていきましょう。
※勝手に名付けたわけではないです(笑)

参考

Stubbing a void method in mockito with side effects

Repository Testカテゴリの最新記事