きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Gradleでマルチモジュールにしている状態から別モジュールのテストコードを共有する

テストのセットアップメソッドや、ファイルからドメインを生成するメソッド等々の便利メソッドを作るとします。マルチモジュールの場合、基本的にはプロダクトコードしか依存しないので、テストの便利メソッドは共有しません。

今回の記事では、テストコードを共有して、テストの便利メソッドを1つのモジュールに書けばすべてのモジュールで使用できる状態にします。

環境

  • gradle
    • 7.4.1

対応

Gradleの依存関係でテストコードを共有するようにします。

testImplementation project(':モジュール名').sourceSets.test.output

// kirimaru-coreの記載したテストコードを共有します
testImplementation project(':kirimaru-core').sourceSets.test.output

ソースコード

kirimaru-repositoryからkirimaru-coreのテストコードを依存させています。

終わりに

各モジュールで似たようなセットアップメソッドを作成していて、非常に面倒だと思っていました。マルチモジュールにしているからしかたないと考えていたのですが、1行で別モジュールから簡単に依存できてよかったです。

まぁ、本音を言えば、JavaでFactory_bot、Factory_boyがあるならもうちょっと楽なんですがね…。

参考情報

【設計論】文字列だと単純にラップした型でも便利

ちょっとポエム寄りのお話です。

プリミティブ型をラップした型を作成することは、対象の業務を表現できることがメリットです。たとえば、主キーのIDが文字列だった場合に、次の制約があった場合でもプリミティブ型では伝えられません。

  • 頭文字3桁がシステム略称名
  • 全体で10桁
    • 例:ABC0000001

他にも本はISBN型を所持しています。ISBNは次の制約を持っていますが、プリミティブ型の場合は伝えられません。

  • 10桁か13桁
  • 末尾1桁がチェック用数値

型を自作すると、システムで使用している大事な概念に対して命名でき、システムで異常値が入らないような制約をかけることができて安全に開発を進められることがメリットです。


今までの私は、業務を表現することのみに対して型を作成しようとしていました。もちろん、それも大事な概念です。しかし、制約を掛けるという点に着目すると、業務を表現しなくてもよいのではないのかと考えました。

Javaの場合、nullの文字列を結合すると「null」という文字列になってしまいます。以前、Javaで文字列結合時に「null」とならないようにするための記事を書きました。

コンストラクタでnullだった場合、空文字に置換するラップ型を作成することで、文字列結合で「null」にならないように回避できます。

public class AppString implements Serializable {
  private String value;

  public AppString(String value){
    if (value == null) {
      this.value = "";
    } else {
      this.value = value;
    }
  }

  public String getValue() {
    return value;
  }
}

ただ、「null」回避のためだけに型を用意するのは変換の手間もあり、可読性が落ちてしまいます。このユースケース単体だと私は採用しません。

他のユースケースとして、バリデーションがあります。自分のシステムでは問題ないものの、帳票印刷するシステムと連携したときに印刷不可能文字が含まれていると、文字化けしてしまうためにバリデーションをかける必要があります。

たとえば、SJISに変換できない文字バリデーションかけるとします。

public class AppString implements Serializable {
  private String value;

  public AppString(String value){
    if (isIllegal(value)) {
      throw new RuntimeException("許容不可能な文字が含まれています");
    }
    if (value == null) {
      this.value = "";
    } else {
      this.value = value;
    }
  }

  public static boolean isIllegal(@Nullable String value) {
    if (value == null) {
      return false;
    }
    try {
      if (!value.equals(new String(value.getBytes("SJIS"), "SJIS"))) {
        return true;
      }
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
    return false;
  }

  public String getValue() {
    return value;
  }
}

このように表現することで、「システムで扱える文字列」であると制約を表現できます。ここまで採用できたら、かなり有用ではないでしょうか。

本来であれば、この概念について適切な名前があるとうれしいのですが、私は思いつきませんでした。アプリケーションの文字列型という意味での「AppString」、システムの文字列型という意味での「SystemString」等々よりも適切な文字列が浮かびませんでした。異常値であれば「禁則文字列」という表現もできるのですが「正常文字列」は意味が分かりませんですしね…。

終わりに

類似情報に記載しているアノテーションでのバリデーションを実装しようとしたときに、バリデーションロジックと概念の凝集度が低かったので、処理を寄せるためのシステム文字列を思いつきました。

一流エンジニアの方々は、どうやって凝集度を高めたり、命名しているんですかね。プリミティブな型の方が使いやすいのは間違いないので、インタフェースで絶対受け取らないようにガードして、ビジネスロジック側はプリミティブ型で扱っているのでしょうか。

完全な思い付きの記事ではあるので、もし便利な点や実際に使ってみての使用感等々を会話してみたいです。

類似情報

一緒に読むと、オススメの記事です。

メソッド呼出回数によって返却値を変更する(正常と例外)【JavaのMockito】

Mockitoを使用して、モックにしたメソッドの呼出回数によって正常な値と例外を返却する方法を残します。

環境

  • Java
    • 17
  • org.mockito.junit.jupiter
    • 4.0.0

ユースケース

  • 1

対応

次の現在日付を返却するメソッドをモックにします。

public interface OffsetDateTimeResolver{
  default OffsetDateTime now() {
    return OffsetDateTime.now(ZoneId.of("Asia/Tokyo"));
  }
}

Mockito.whenを使用して、パラメータにてモック対象のメソッドを呼び出します。

そのままメソッドチェインでthenReturnを使用すると、正常系の値を返却します。第二パラメータ以降を可変長引数で渡している場合、パラメータの順番と呼出回数による返却値が一致します。

同様にthenThrowを使用すると、異常系の値を返却します。可変長引数も同様の挙動です。

次のコードの例は1回目が正常系、2回目が正常系、3回目が異常系、4回目が正常系、5回目が異常系を返却するテストです。

  @Mock
  private OffsetDateTimeResolver offsetDateTimeResolver;

  @DisplayName("1回目OK, 2回目OK, 3回目NG, 4回目OK, 5回目NG")
  @Test
  void _1OK_2OK_3NG_4OK_5NG() {
    // GIVEN
    OffsetDateTime now = OffsetDateTime.now(); // 現在時刻
    when(offsetDateTimeResolver.now())
        .thenReturn(now, now) // 1回目, 2回目
        .thenThrow(new RuntimeException()) // 3回目
        .thenReturn(now) // 4回目
        .thenThrow(new RuntimeException()); // 5回目

    // THEN
    // 1回目
    assertThat(offsetDateTimeResolver.now()).isEqualTo(now);
    // 2回目
    assertThat(offsetDateTimeResolver.now()).isEqualTo(now);
    // 3回目
    assertThatThrownBy(() -> offsetDateTimeResolver.now()).isInstanceOf(RuntimeException.class);
    // 4回目
    assertThat(offsetDateTimeResolver.now()).isEqualTo(now);
    // 5回目
    assertThatThrownBy(() -> offsetDateTimeResolver.now()).isInstanceOf(RuntimeException.class);
  }

備考

Mockito.whenにモック対象のメソッドをパラメータに渡すたびにモック設定がリセットされます。

次のコードの例は正常系の設定をした後に、異常系の設定をし直しているので、1回目から異常系の値を返却してしまっています。

  @Test
  void resetされる() {
    // GIVEN
    OffsetDateTime now = OffsetDateTime.now();
    when(offsetDateTimeResolver.now()).thenReturn(now);
    when(offsetDateTimeResolver.now()).thenThrow(new RuntimeException(ERROR_MESSAGE));

    // THEN
    // 1回目が正常系を返却しない
    assertThatThrownBy(() -> offsetDateTimeResolver.now()).isInstance(RuntimeException.class)
    assertThatThrownBy(() -> offsetDateTimeResolver.now()).isInstance(RuntimeException.class)
  }

なお、あくまでソースコードは例です。

実際、上の書き方だとアサーションルーレットと呼ばれるよろしくない書き方になってしまうので、この記事を参考にしてSoftAssertionsを使用してみませんか?

ソースコード

終わりに

Mockitoにて正常系の後に異常系、異常系の後に正常系を返却できるということを勉強できてよかったです。

実際はここまで複雑なケースは出会わないかもしれないのですが、当処理で解消できそうなツイートがありましたので、出会う確率は0ではないと思います。

この記事を書くきっかけになったツイート

twitter.com

類似記事