1. 概要

ソフトウェアが環境変数やシステムプロパティなどのシステムリソースに依存している場合、または System.exit などのプロセスレベルの操作を使用している場合、ソフトウェアをテストするのは難しい場合があります。

Javaは、環境変数を設定するための直接的な方法を提供していません。あるテストで設定された値が別のテストの実行に影響を与えるリスクがあります。 同様に、 System.exit を実行する可能性のあるコードに対して、テストを中止する可能性があるため、JUnitテストの記述を避けていることに気付く場合があります。

システムルールとシステムラムダライブラリは、これらの問題の初期の解決策でした。 このチュートリアルでは、 SystemStubsと呼ばれるSystemLambdaの新しいフォークを見ていきます。これは、JUnit5の代替手段を提供します。

2. なぜシステムスタブ?

2.1. SystemLambdaはJUnitプラグインではありません

元のシステムルールライブラリは、JUnit4でのみ使用可能でした。 JUnit5のJUnitVintage でも引き続き使用できますが、JUnit4テストを継続的に作成する必要がありました。 ライブラリの作成者は、 System Lambda と呼ばれるテストフレームワークにとらわれないバージョンを作成しました。これは、各テストメソッド内での使用を目的としています。

@Test
void aSingleSystemLambda() throws Exception {
    restoreSystemProperties(() -> {
        System.setProperty("log_dir", "test/resources");
        assertEquals("test/resources", System.getProperty("log_dir"));
    });

    // more test code here
}

テストコードはラムダとして表現され、必要なスタブを設定するメソッドに渡されます。 クリーンアップは、コントロールが残りのテストメソッドに戻る直前に行われます。

これは場合によってはうまく機能しますが、このアプローチにはいくつかの欠点があります。

2.2. 余分なコードの回避

System Lambdaアプローチの利点は、特定のタイプのテストを実行するためのファクトリクラス内にいくつかの一般的なレシピがあることです。 ただし、これは、多くのテストケースで使用したい場合に、コードの膨張につながります。

まず、テストコード自体がチェックされた例外をスローしない場合でも、ラッパーメソッドはスローするため、すべてのメソッドが throwsExceptionを取得します。 次に、複数のテストに同じルールを設定するには、コードを複製する必要があります。 各テストは、同じ構成を個別に実行する必要があります。

ただし、このアプローチの最も厄介な側面は、一度に複数のツールをセットアップしようとする場合に発生します。 いくつかの環境変数とシステムプロパティを設定するとします。 テストコードを開始する前に、2つのレベルのネストが必要になります。

@Test
void multipleSystemLambdas() throws Exception {
    restoreSystemProperties(() -> {
        withEnvironmentVariable("URL", "https://www.baeldung.com")
            .execute(() -> {
                System.setProperty("log_dir", "test/resources");
                assertEquals("test/resources", System.getProperty("log_dir"));
                assertEquals("https://www.baeldung.com", System.getenv("URL"));
            });
    });
}

これは、JUnitプラグインまたは拡張機能が、テストで必要なコードの量を削減するのに役立つ場所です。

2.3. より少ないボイラープレートの使用

最小限の定型文でテストを記述できることを期待する必要があります。

@SystemStub
private EnvironmentVariables environmentVariables = ...;

@SystemStub
private SystemProperties restoreSystemProperties;

@Test
void multipleSystemStubs() {
    System.setProperty("log_dir", "test/resources");
    assertEquals("test/resources", System.getProperty("log_dir"));
    assertEquals("https://www.baeldung.com", System.getenv("ADDRESS"));
}

このアプローチは、 SystemStubs JUnit 5拡張機能によって提供され、より少ないコードでテストを構成できます。

2.4. ライフサイクルフックをテストする

使用可能なツールがexecute-aroundpattern のみの場合、スタブ動作をテストライフサイクルのすべての部分にフックすることは不可能です。 これは、@SpringBootTestなどの他のJUnit拡張機能と組み合わせようとする場合に特に困難です。

Spring Bootテストの周囲にいくつかの環境変数を設定したい場合、そのテストエコシステム全体を単一のテストメソッド内に合理的に埋め込む方法はありません。 テストスイートの周囲でテストセットアップをアクティブ化する方法が必要になります。

これは、System Lambdaで採用されている方法では不可能であり、システムスタブを作成する主な理由の1つでした。

2.5. 動的プロパティを奨励する

JUnit Pioneer など、システムプロパティを設定するための他のフレームワークは、コンパイル時に既知の構成を強調します。 TestcontainersまたはWiremockを使用している可能性がある最新のテストでは、これらのツールの起動後にランダムなランタイム設定に基づいてシステムプロパティを設定する必要があります。 これは、テストライフサイクル全体で使用できるテストライブラリで最適に機能します。

2.6. より多くの構成可能性

catchSystemExit のように、テストコードをラップアラウンドして単一のジョブを実行する既製のテストレシピがあると便利です。 ただし、これは、必要になる可能性のある構成オプションの各バリエーションを提供するために、テストライブラリの開発者に依存しています。

構成による構成はより柔軟であり、新しいシステムスタブの実装の大部分を占めています。

ただし、 System Stubsは、下位互換性のためにSystemLambdaの元のテスト構成をサポートしています。 さらに、新しいJUnit 5拡張機能、一連のJUnit 4ルール、およびその他の多くの構成オプションを提供します。 元のコードに基づいていますが、より豊富な機能セットを提供するために大幅にリファクタリングおよびモジュール化されています。

それについてもっと学びましょう。

3. 入門

3.1. 依存関係

JUnit 5拡張機能には、適度に最新バージョンの JUnit5が必要です。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>

すべてのSystemStubsライブラリの依存関係をpom.xmlに追加しましょう。

<!-- for testing with only lambda pattern -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-core</artifactId>
    <version>1.1.0</version>
    <scope>test</scope>
</dependency>

<!-- for JUnit 4 -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-junit4</artifactId>
    <version>1.1.0</version>
    <scope>test</scope>
</dependency>

<!-- for JUnit 5 -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-jupiter</artifactId>
    <version>1.1.0</version>
    <scope>test</scope>
</dependency>

使用しているテストフレームワークに必要な数だけインポートする必要があることに注意してください。 実際、後者の2つはどちらも、コアの依存関係を一時的に含んでいます。

それでは、最初のテストを書いてみましょう。

3.2. JUnit4環境変数

タイプEnvironmentVariablesRuleのテストクラスでJUnit4 @Rule 注釈付きフィールドを宣言することにより、環境変数を制御できます。 これは、テストの実行時にJUnit 4によってアクティブ化され、テスト内で環境変数を設定できるようになります。

@Rule
public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();

@Test
public void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariablesRule.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

実際には、 @Before メソッドで環境変数の値を設定して、設定をすべてのテストで共有できるようにすることをお勧めします。

@Before
public void before() {
    environmentVariablesRule.set("ENV", "value1")
      .set("ENV2", "value2");
}

ここでは、流暢な設定メソッドを使用していることに注意してください。これにより、メソッドチェーンを介して複数の値を簡単に設定できます。

EnvironmentVariablesRule オブジェクトのコンストラクターを使用して、構築時に値を提供することもできます。

@Rule
public EnvironmentVariablesRule environmentVariablesRule =
  new EnvironmentVariablesRule("ENV", "value1",
    "ENV2", "value2");

コンストラクターにはいくつかのオーバーロードがあり、変数をさまざまな形式で提供できます。 上記の例では、varargsを使用して任意の数の名前と値のペアを提供できます。

各システムスタブJUnit4ルールは、コアスタブオブジェクトの1つのサブクラスです。 また、 static フィールドに@ClassRuleアノテーションを指定して、テストクラス全体のライフサイクル全体で使用することもできます。これにより、最初のテストの前にアクティブ化され、その後クリーンアップされます。最後の直後に。

3.3. JUnit5環境変数

JUnit5テスト内でSystemStubsオブジェクトを使用する前に、テストクラスに拡張機能を追加する必要があります。

@ExtendWith(SystemStubsExtension.class)
class EnvironmentVariablesJUnit5 {
    // tests
}

次に、JUnit5のテストクラスにフィールドを作成して管理します。 これに@SystemStubの注釈を付けて、拡張機能がアクティブ化できるようにします。

@SystemStub
private EnvironmentVariables environmentVariables;

拡張機能は、 @SystemStub でマークされたオブジェクトのみを管理します。これにより、必要に応じて、テストで他のSystemStubsオブジェクトを手動で使用できます。

ここでは、スタブオブジェクトの構成は提供していません。 Mockito拡張機能がモックを構築するのと同じ方法で、拡張機能が1つを構築します。

これで、オブジェクトを使用して、テストの1つに環境変数を設定できます。

@Test
void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

テストメソッドの外部からすべてのテストに適用される環境変数を提供する場合は、 @BeforeEach メソッドの内部で行うか、EnvironmentVariablesのコンストラクターを使用して設定できます。私達の価値:

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables("ENV", "value1");

EnvironmentVariablesRule と同様に、コンストラクターにはいくつかのオーバーロードがあり、目的の変数を設定するためのさまざまな方法が可能です。 必要に応じて、 set メソッドを流暢に使用して、値を設定することもできます。

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables()
    .set("ENV", "value1")
    .set("ENV2", "value2");

フィールドをstaticにして、 @BeforeAll / @AfterAllライフサイクルの一部として管理することもできます。

3.4. JUnit5パラメーターインジェクション

スタブオブジェクトをフィールドに配置することは、すべてのテストでスタブオブジェクトを使用する場合に役立ちますが、選択したオブジェクトにのみ使用することをお勧めします。 これは、JUnit5パラメーターインジェクションによって実現できます。

@Test
void givenEnvironmentCanBeModified(EnvironmentVariables environmentVariables) {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

この場合、 EnvironmentVariables オブジェクトは、デフォルトのコンストラクターを使用して構築され、単一のテスト内で使用できるようになりました。 オブジェクトは、ランタイム環境で動作するようにアクティブ化されています。 テストが終了すると片付けられます。

すべてのSystemStubsオブジェクトには、デフォルトのコンストラクターと、実行中に再構成できる機能があります。 テストには必要な数だけ注入できます。

3.5. 実行環境変数

スタブを作成するための元のSystemLambdaファサードメソッドは、SystemStubsクラスからも利用できます。 内部的には、スタブオブジェクトのインスタンスを作成することで実装されます。 レシピから返されるオブジェクトは、さらに構成して使用するためのスタブオブジェクトである場合があります。

withEnvironmentVariable("ENV3", "val")
    .execute(() -> {
        assertThat(System.getenv("ENV3")).isEqualTo("val");
    });

舞台裏では、withEnvironmentVariableは次と同等のことを行っています。

return new EnvironmentVariables().set("ENV3", "val");

executeメソッドはすべてのSystemStubオブジェクトに共通です。オブジェクトによって定義されたスタブを設定し、渡されたラムダを実行します。 その後、それは片付けられ、制御を周囲のテストに戻します。

テストコードが値を返す場合、その値はexecuteによって返されます。

String extracted = new EnvironmentVariables("PROXY", "none")
  .execute(() -> System.getenv("PROXY"));

assertThat(extracted).isEqualTo("none");

これは、テストしているコードが何かを構築するために環境設定にアクセスする必要がある場合に役立ちます。 AWSLambdaハンドラーなどのテスト時によく使用されます。これらは環境変数を介して構成されることがよくあります。

不定期のテストでのこのパターンの利点は、必要な場合にのみ、スタブを明示的に設定する必要があることです。 したがって、より正確で目に見えるようにすることができます。 ただし、テスト間でセットアップを共有することはできず、より時間がかかる可能性があります。

3.6. 複数のシステムスタブ

JUnit4およびJUnit5プラグインがスタブオブジェクトを構築およびアクティブ化する方法については、すでに説明しました。 複数のスタブがある場合、それらはフレームワークコードによって適切に設定および破棄されます。

ただし、execute-aroundパターンのスタブオブジェクトを作成する場合、それらすべての内部で実行するテストコードが必要です。

これは、 with / executeメソッドを使用して実現できます。 これらは、単一のexecuteで使用される複数のスタブオブジェクトからコンポジットを作成することによって機能します。

with(new EnvironmentVariables("FOO", "bar"), new SystemProperties("prop", "val"))
  .execute(() -> {
      assertThat(System.getenv("FOO")).isEqualTo("bar");
      assertThat(System.getProperty("prop")).isEqualTo("val");
  });

これで、JUnitフレームワークのサポートがある場合とない場合の両方でSystem Stubsオブジェクトを使用する一般的な形式を見てきました。次に、ライブラリの残りの機能を見てみましょう。

4. システムプロパティ

JavaではいつでもSystem.setPropertyを呼び出すことができます。 ただし、これにより、あるテストから別のテストに設定がリークするリスクがあります。 SystemProperties スタブの主な目的は、テストの完了後にシステムプロパティを元の設定に復元することです。 ただし、一般的なセットアップコードでは、テストを開始する前に使用するシステムプロパティを定義することも役立ちます。

4.1. JUnit4システムのプロパティ

ルールをJUnit4テストクラスに追加することで、他のテストメソッドで行われたSystem.setProperty呼び出しから各テストを分離できます。 コンストラクターを介していくつかの先行プロパティを提供することもできます。

@Rule
public SystemPropertiesRule systemProperties =
  new SystemPropertiesRule("db.connection", "false");

このオブジェクトを使用して、JUnit @Beforeメソッドでいくつかの追加プロパティを設定することもできます。

@Before
public void before() {
    systemProperties.set("before.prop", "before");
}

テストの本体でsetメソッドを使用することも、必要に応じてSystem.setPropertyを使用することもできます。 set は、 SystemPropertiesRule の作成、または @Before メソッドでのみ使用する必要があります。これは、設定をルールに保存し、後で適用できるようにするためです。

4.2. JUnit5システムのプロパティ

SystemPropertiesオブジェクトを使用する主なユースケースは2つあります。 各テストケースの後でシステムプロパティをリセットしたり、各テストケースで使用するために中央の場所にいくつかの一般的なシステムプロパティを準備したりすることができます。

システムプロパティを復元するには、JUnit5拡張機能とSystemPropertiesフィールドの両方をテストクラスに追加する必要があります。

@ExtendWith(SystemStubsExtension.class)
class RestoreSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

}

これで、各テストには、変更したシステムプロパティが後でクリーンアップされます。

パラメータインジェクションにより、選択したテストに対してこれを行うこともできます。

@Test
void willRestorePropertiesAfter(SystemProperties systemProperties) {

}

テストにプロパティを設定する場合は、 SystemProperties オブジェクトの構築でそれらのプロパティを割り当てるか、@BeforeEachメソッドを使用できます。

@ExtendWith(SystemStubsExtension.class)
class SetSomeSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

    @BeforeEach
    void before() {
        systemProperties.set("beforeProperty", "before");
    }
}

繰り返しになりますが、JUnit5テストには注釈を付ける必要があることに注意してください。 @ExtendWith(SystemStubsExtension.class)。 提供しない場合、拡張機能はSystemStubsオブジェクトを作成します新着イニシャライザリストのステートメント。

4.3. ExecuteAroundを使用したシステムプロパティ

SystemStubs クラスは、 restoreSystemProperties メソッドを提供して、プロパティが復元されたテストコードを実行できるようにします。

restoreSystemProperties(() -> {
    // test code
    System.setProperty("unrestored", "true");
});

assertThat(System.getProperty("unrestored")).isNull();

これは何も返さないラムダを取ります。 共通のセットアップ関数を使用してプロパティを作成する場合は、テストメソッドから戻り値を取得するか、 with / execute[を介してSystemPropertiesを他のスタブと組み合わせます。 X189X]、オブジェクトを明示的に作成できます。

String result = new SystemProperties()
  .execute(() -> {
      System.setProperty("unrestored", "true");
      return "it works";
  });

assertThat(result).isEqualTo("it works");
assertThat(System.getProperty("unrestored")).isNull();

4.4. ファイルのプロパティ

SystemPropertiesオブジェクトとEnvironmentVariablesオブジェクトはどちらも、Mapから構築できます。 これにより、Javaの Properties オブジェクトを、システムプロパティまたは環境変数のソースとして提供できます。

PropertySource クラス内には、ファイルまたはリソースからJavaプロパティをロードするためのヘルパーメソッドがあります。 これらのプロパティファイルは、名前と値のペアです。

name=baeldung
version=1.0

fromResource 関数を使用して、リソースtest.propertiesからロードできます。

SystemProperties systemProperties =
  new SystemProperties(PropertySource.fromResource("test.properties"));

PropertySource には、fromFilefromInputStreamなど、他のソース用の同様の便利なメソッドがあります。

5. システム出力とシステムエラー

アプリケーションがSystem.out、に書き込む場合、テストが難しい場合があります。 これは、出力のターゲットとしてインターフェイスを使用し、テスト時にそれをモックすることで解決される場合があります。

interface LogOutput {
   void write(String line);
}

class Component {
    private LogOutput log;

    public void method() {
        log.write("Some output");
    }
}

このような手法はMockitoモックでうまく機能しますが、System.out自体をトラップできる場合は必要ありません。

5.1. JUnit 4 SystemOutRuleおよびSystemErrRule

JUnit4テストでSystem.outに出力をトラップするには、SystemOutRuleを追加します。

@Rule
public SystemOutRule systemOutRule = new SystemOutRule();

その後、System.outへの出力をテスト内で読み取ることができます。

System.out.println("line1");
System.out.println("line2");

assertThat(systemOutRule.getLines())
  .containsExactly("line1", "line2");

テキストの形式は選択できます。 上記の例では、 ストリームによって提供された getLines 。 テキストのブロック全体を取得することもできます。

assertThat(systemOutRule.getText())
  .startsWith("line1");

ただし、このテキストにはプラットフォーム間で異なる改行文字が含まれることに注意してください。 正規化された形式を使用することにより、すべてのプラットフォームで改行を \nに置き換えることができます。

assertThat(systemOutRule.getLinesNormalized())
  .isEqualTo("line1\nline2\n");

SystemErrRule は、 System.err に対して、対応するSystem.outと同じように機能します。

@Rule
public SystemErrRule systemErrRule = new SystemErrRule();

@Test
public void whenCodeWritesToSystemErr_itCanBeRead() {
    System.err.println("line1");
    System.err.println("line2");

    assertThat(systemErrRule.getLines())
      .containsExactly("line1", "line2");
}

SystemErrAndOutRule クラスもあり、System.outSystem.errの両方を同時に単一のバッファーにタップします。

5.2. JUnit5の例

他のSystemStubsオブジェクトと同様に、SystemOutまたはSystemErrタイプのフィールドまたはパラメーターを宣言するだけで済みます。 これにより、出力のキャプチャが提供されます。

@SystemStub
private SystemOut systemOut;

@SystemStub
private SystemErr systemErr;

@Test
void whenWriteToOutput_thenItCanBeAsserted() {
    System.out.println("to out");
    System.err.println("to err");

    assertThat(systemOut.getLines()).containsExactly("to out");
    assertThat(systemErr.getLines()).containsExactly("to err");
}

SystemErrAndOut クラスを使用して、両方の出力セットを同じバッファーに送ることもできます。

5.3. 実行の例

SystemStubs ファサードは、出力をタップしてStringとして返すためのいくつかの関数を提供します。

@Test
void givenTapOutput_thenGetOutput() throws Exception {
    String output = tapSystemOutNormalized(() -> {
        System.out.println("a");
        System.out.println("b");
    });

    assertThat(output).isEqualTo("a\nb\n");
}

これらのメソッドは、生のオブジェクト自体ほど豊富なインターフェイスを提供しないことに注意してください。 出力のキャプチャは、環境変数の設定など、他のスタブと簡単に組み合わせることができません。

ただし、 SystemOut SystemErr、、およびSystemErrAndOutオブジェクトは直接使用できます。 たとえば、それらをいくつかのSystemPropertiesと組み合わせることができます。

SystemOut systemOut = new SystemOut();
SystemProperties systemProperties = new SystemProperties("a", "!");
with(systemOut, systemProperties)
  .execute(()  -> {
    System.out.println("a: " + System.getProperty("a"));
});

assertThat(systemOut.getLines()).containsExactly("a: !");

5.4. ミューティング

時々、私たちの目的は出力をキャプチャすることではなく、テスト実行ログを乱雑にしないことです。 これは、muteSystemOutまたはmuteSystemErr関数を使用して実現できます。

muteSystemOut(() -> {
    System.out.println("nothing is output");
});

JUnit 4 SystemOutRule を介して、すべてのテストで同じことを達成できます。

@Rule
public SystemOutRule systemOutRule = new SystemOutRule(new NoopStream());

JUnit 5では、同じ手法を使用できます。

@SystemStub
private SystemOut systemOut = new SystemOut(new NoopStream());

5.5. カスタマイズ

これまで見てきたように、出力をインターセプトするためのいくつかのバリエーションがあります。 それらはすべて、ライブラリ内の共通の基本クラスを共有します。 便宜上、 SystemErrAndOut、などのいくつかのヘルパーメソッドとタイプは、一般的なことを行うのに役立ちます。 ただし、ライブラリ自体は簡単にカスタマイズできます。

Output の実装として、出力をキャプチャするための独自のターゲットを提供できます。 最初の例では、OutputクラスTapStreamが使用されていることをすでに確認しています。 NoopStreamはミューティングに使用されます。 DisallowWriteStream もあり、何かが書き込まれるとエラーをスローします。

// throws an exception:
new SystemOut(new DisallowWriteStream())
  .execute(() -> System.out.println("boo"));

6. のモッキングシステム

stdinの入力を読み取るアプリケーションがあるかもしれません。 これをテストするには、 InputStream から読み取る関数にアルゴリズムを抽出し、事前に準備された入力ストリームをフィードする必要があります。 一般的に、モジュラーコードの方が優れているため、これは適切なパターンです。

ただし、コア関数のみをテストすると、System.inをソースとして提供するコードのテストカバレッジが失われます。

いずれにせよ、独自のストリームを構築するのは不便な場合があります。 幸いなことに、SystemStubsにはこれらすべてのソリューションがあります。

6.1. 入力ストリームをテストする

System Stubsは、InputStream から読み取るコードの代替入力として、AltInputStreamクラスのファミリーを提供します。

LinesAltStream testInput = new LinesAltStream("line1", "line2");

Scanner scanner = new Scanner(testInput);
assertThat(scanner.nextLine()).isEqualTo("line1");

この例では、文字列の配列を使用して構築しました LinesAltStream 、 しかしストリームからの入力を提供できたはずです 、これを任意のテキストデータソースで使用できるようにします必ずしもすべてを一度にメモリにロードする必要はありません。

6.2. JUnit4の例

SystemInRule を使用して、JUnit4テストに入力する行を提供できます。

@Rule
public SystemInRule systemInRule =
  new SystemInRule("line1", "line2", "line3");

次に、テストコードはSystem.inからこの入力を読み取ることができます。

@Test
public void givenInput_canReadFirstLine() {
    assertThat(new Scanner(System.in).nextLine())
      .isEqualTo("line1");
}

6.3. JUnit5の例

JUnit 5テストでは、SystemInフィールドを作成します。

@SystemStub
private SystemIn systemIn = new SystemIn("line1", "line2", "line3");

次に、 System.in を使用してテストを実行し、これらの行を入力として提供します。

6.4. 実行の例

SystemStubs ファサードは、executeメソッドで使用するSystemInオブジェクトを作成するファクトリメソッドとしてwithTextFromSystemInを提供します。

withTextFromSystemIn("line1", "line2", "line3")
  .execute(() -> {
      assertThat(new Scanner(System.in).nextLine())
        .isEqualTo("line1");
  });

6.5. カスタマイズ

SystemIn オブジェクトは、構築中またはテスト内で実行中に、さらに多くの機能を追加できます。

andExceptionThrownOnInputEnd を呼び出すことができます。これにより、 System.in からの読み取りで、テキストが不足したときに例外がスローされます。 これにより、ファイルからの読み取りの中断をシミュレートできます。

setInputStream を使用して、 FileInputStream など、任意のInputStreamからの入力ストリームを設定することもできます。 また、入力テキストを操作するLinesAltStreamTextAltStreamもあります。

7. モッキングシステム。終了

前述のように、コードが System.exit を呼び出すことができる場合、テスト障害のデバッグが危険で困難になる可能性があります。 System.exit をスタブ化する目的の1つは、追跡可能なエラーを誤って呼び出すことです。 もう1つの動機は、ソフトウェアからの意図的な終了をテストすることです。

7.1. JUnit4の例

System.exit がJVMを停止しないようにするための安全対策として、SystemExitRuleをテストクラスに追加しましょう。

@Rule
public SystemExitRule systemExitRule = new SystemExitRule();

ただし、正しい終了コードが使用されたかどうかも確認したい場合があります。 そのためには、コードが AbortExecutionException をスローすることを表明する必要があります。これは、System.exitが呼び出されたというSystemStubsシグナルです。

@Test
public void whenExit_thenExitCodeIsAvailable() {
    assertThatThrownBy(() -> {
        System.exit(123);
    }).isInstanceOf(AbortExecutionException.class);

    assertThat(systemExitRule.getExitCode()).isEqualTo(123);
}

この例では、AssertJassertThatThrownByを使用して、発生した例外シグナリングの終了をキャッチして確認しました。 次に、SystemExitRuleからgetExitCodeを調べて、終了コードをアサートしました。

7.2. JUnit5の例

JUnit 5テストでは、@SystemStubフィールドを宣言します。

@SystemStub
private SystemExit systemExit;

次に、JUnit4のSystemExitRule と同じ方法で、SystemExitクラスを使用します。 SystemExitRuleクラスがSystemExitのサブクラスであるとすると、それらは同じインターフェースを持ちます。

7.3. 実行の例

SystemStubs クラスは、 catchSystemExit、を提供します。これは、SystemExitexecute関数を内部的に使用します。

int exitCode = catchSystemExit(() -> {
    System.exit(123);
});
assertThat(exitCode).isEqualTo(123);

JUnitプラグインの例と比較すると、このコードはシステムの終了を示す例外をスローしません。 代わりに、エラーをキャッチして終了コードを記録します。 facadeメソッドを使用すると、終了コードが返されます。

execute メソッドを直接使用すると、終了がキャッチされ、終了コードがSystemExitオブジェクト内に設定されます。 次に、 getExitCode を呼び出して終了コードを取得するか、nullを呼び出して終了コードを取得します。

8. JUnit5のカスタムテストリソース

JUnit 4は、システムスタブで使用されるようなテストルールを作成するための単純な構造をすでに提供しています。 セットアップとティアダウンを使用して、リソースの新しいテストルールを作成する場合は、 ExternalResource をサブクラス化し、beforeおよびafterメソッドのオーバーライドを提供できます。 。

JUnit 5には、リソース管理のためのより複雑なパターンがあります。 単純なユースケースの場合、開始点としてSystemStubsライブラリを使用することができます。 SystemStubsExtension は、TestResourceインターフェースを満たすものすべてで動作します。

8.1. TestResourceの作成

TestResource のサブクラスを作成してから、システムスタブと同じ方法でカスタムオブジェクトを使用できます。 フィールドとパラメーターの自動作成を使用する場合は、デフォルトのコンストラクターを提供する必要があることに注意してください。

いくつかのテストのためにデータベースへの接続を開き、後でそれを閉じたいとしましょう。

public class FakeDatabaseTestResource implements TestResource {
    // let's pretend this is a database connection
    private String databaseConnection = "closed";

    @Override
    public void setup() throws Exception {
        databaseConnection = "open";
    }

    @Override
    public void teardown() throws Exception {
        databaseConnection = "closed";
    }

    public String getDatabaseConnection() {
        return databaseConnection;
    }
}

databaseConnection 文字列を、データベース接続などのリソースの例として使用しています。 セットアップメソッドとティアダウンメソッドでリソースの状態を変更します。

8.2. Execute-Aroundが組み込まれています

次に、execute-aroundパターンでこれを使用してみましょう。

FakeDatabaseTestResource fake = new FakeDatabaseTestResource();
assertThat(fake.getDatabaseConnection()).isEqualTo("closed");

fake.execute(() -> {
    assertThat(fake.getDatabaseConnection()).isEqualTo("open");
});

ご覧のとおり、 TestResource インターフェースは、他のオブジェクトの実行機能を提供しました。

8.3. JUnit5テストのカスタムTestResource

これは、JUnit5テスト内でも使用できます。

@ExtendWith(SystemStubsExtension.class)
class FakeDatabaseJUnit5UnitTest {

    @Test
    void useFakeDatabase(FakeDatabaseTestResource fakeDatabase) {
        assertThat(fakeDatabase.getDatabaseConnection()).isEqualTo("open");
    }
}

したがって、システムスタブの設計に従う追加のテストオブジェクトを簡単に作成できます。

9. JUnit5スプリングテストの環境とプロパティのオーバーライド

Springテストの環境変数の設定は難しい場合があります。 Springが取得するシステムプロパティを設定するために、統合テスト用のカスタムルール作成する場合があります。

ApplicationContextInitializerクラスを使用してSpringContextにプラグインし、テスト用の追加のプロパティを提供することもできます。

多くのSpringアプリケーションは、システムプロパティまたは環境変数のオーバーライドによって制御されるため、Springテストを内部クラスとして実行し、システムスタブを使用してこれらを外部テストに設定する方が簡単な場合があります。

SystemStubsのドキュメントに完全な例があります。 まず、外部クラスを作成します。

@ExtendWith(SystemStubsExtension.class)
public class SpringAppWithDynamicPropertiesTest {

    // sets the environment before Spring even starts
    @SystemStub
    private static EnvironmentVariables environmentVariables;
}

この場合、@ SystemStubフィールドはstaticであり、@BeforeAllメソッドで初期化されます。

@BeforeAll
static void beforeAll() {
     String baseUrl = ...;

     environmentVariables.set("SERVER_URL", baseUrl);
}

テストライフサイクルのこの時点で、Springテストを実行する前に、いくつかのグローバルリソースを作成し、実行環境に適用できます。

次に、Springテストを@Nestedクラスに入れることができます。 これにより、親クラスが設定されている場合にのみ実行されます。

@Nested
@SpringBootTest(classes = {RestApi.class, App.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class InnerSpringTest {
    @LocalServerPort
    private int serverPort;

    // Test methods
}

Springコンテキストは、外部クラスの@ SystemStubオブジェクトによって設定された環境の状態に対して作成されます。

この手法により、SpringBeanの背後で実行されている可能性のあるシステムプロパティまたは環境変数の状態に依存する他のライブラリの構成を制御することもできます。

これにより、Springテストを実行する前に、テストライフサイクルにフックして、プロキシ設定やHTTP接続プールパラメーターなどを変更できます。

10. 結論

この記事では、システムリソースをモックできることの重要性と、SystemStubsがJUnit4およびJUnit5プラグインを介したコードの繰り返しを最小限に抑えてスタブの複雑な構成を可能にする方法について説明しました。

テストでは、環境変数とシステムプロパティを提供および分離する方法を確認しました。 次に、出力をキャプチャし、標準ストリームで入力を制御することを検討しました。 また、System.exitへの呼び出しのキャプチャとアサートについても調べました。

最後に、カスタムテストリソースを作成する方法と、Springでシステムスタブを使用する方法について説明しました。

いつものように、例の完全なソースコードは、GitHubから入手できます。