Java:System.exit()を呼び出すメソッドをテストする方法は?


198

System.exit()特定の入力を呼び出す必要があるいくつかのメソッドがあります。残念ながら、これらのケースをテストすると、JUnitが終了します。System.exit()現在のスレッドだけでなく、JVMを終了するため、メソッド呼び出しを新しいスレッドに配置しても効果がないようです。これに対処するための一般的なパターンはありますか?たとえば、スタブを代用できSystem.exit()ますか?

[編集]問題のクラスは、実際には私がJUnit内でテストしようとしているコマンドラインツールです。たぶんJUnitは単に仕事に適したツールではありませんか?補完的な回帰テストツールの提案を歓迎します(できれば、JUnitやEclEmmaとうまく統合できるものが望ましい)。


2
私は...()関数は、これまでにSystem.exitを呼ぶだろう理由として好奇心が強い
トーマスオーウェンズ

2
アプリケーションを終了する関数を呼び出している場合。たとえば、ユーザーが続けてx回以上実行することを許可されていないタスクを実行しようとした場合、ユーザーをアプリケーションから強制的に除外します。
エリー

5
その場合でも、アプリケーションから戻るには、System.exit()よりも良い方法があるはずだと私はまだ考えています。
Thomas Owens、

27
main()をテストしている場合は、System.exit()を呼び出すのが最適です。私たちは、エラー時にバッチ処理が0と1で、成功の終了時に終了する必要があることを要求してい
マシュー・ファーウェル

5
System.exit()は悪いと言う人には同意しません-プログラムはすぐに失敗するはずです。例外をスローしても、開発者が終了したい状況でアプリケーションが無効な状態になり、誤ったエラーが発生するだけです。
Sridhar Sarnobat

回答:


216

実際、Derkeiler.comは次のことを提案しています。

  • なんでSystem.exit()

System.exit(whateverValue)で終了する代わりに、未チェックの例外をスローしませんか?通常の使用では、JVMのラストディッチキャッチャーまでずっと移動し、スクリプトをシャットダウンします(途中でどこかでキャッチすることに決めた場合を除き、いつか役立つかもしれません)。

JUnitシナリオでは、JUnitフレームワークによってキャッチされます。JUnitフレームワークは、そのようなテストが失敗したことを報告し、次のテストにスムーズに進みます。

  • PreventがSystem.exit()、実際にJVMを終了します。

System.exitの呼び出しを防止するセキュリティマネージャーで実行するようにTestCaseを変更してから、SecurityExceptionをキャッチしてください。

public class NoExitTestCase extends TestCase 
{

    protected static class ExitException extends SecurityException 
    {
        public final int status;
        public ExitException(int status) 
        {
            super("There is no escape!");
            this.status = status;
        }
    }

    private static class NoExitSecurityManager extends SecurityManager 
    {
        @Override
        public void checkPermission(Permission perm) 
        {
            // allow anything.
        }
        @Override
        public void checkPermission(Permission perm, Object context) 
        {
            // allow anything.
        }
        @Override
        public void checkExit(int status) 
        {
            super.checkExit(status);
            throw new ExitException(status);
        }
    }

    @Override
    protected void setUp() throws Exception 
    {
        super.setUp();
        System.setSecurityManager(new NoExitSecurityManager());
    }

    @Override
    protected void tearDown() throws Exception 
    {
        System.setSecurityManager(null); // or save and restore original
        super.tearDown();
    }

    public void testNoExit() throws Exception 
    {
        System.out.println("Printing works");
    }

    public void testExit() throws Exception 
    {
        try 
        {
            System.exit(42);
        } catch (ExitException e) 
        {
            assertEquals("Exit status", 42, e.status);
        }
    }
}

2012年12月の更新:

ウィルが提案しているコメントで使用してシステムのルールを、JUnitの(4.9+)のコレクションは、使用するコードをテストするためのルールjava.lang.System
これは、2011年12月の回答Stefan Birknerによって最初に言及されまし

System.exit(…)

ExpectedSystemExitルールを使用して、それSystem.exit(…)が呼び出されていることを確認します。
終了ステータスも確認できます。

例えば:

public void MyTest {
    @Rule
    public final ExpectedSystemExit exit = ExpectedSystemExit.none();

    @Test
    public void noSystemExit() {
        //passes
    }

    @Test
    public void systemExitWithArbitraryStatusCode() {
        exit.expectSystemExit();
        System.exit(0);
    }

    @Test
    public void systemExitWithSelectedStatusCode0() {
        exit.expectSystemExitWithStatus(0);
        System.exit(0);
    }
}

4
最初の答えは好きではありませんでしたが、2番目はかなりクールです。私はセキュリティマネージャーに手を出していないので、それよりも複雑だと思いました。ただし、セキュリティマネージャー/テストメカニズムをどのようにテストしますか。
ビルK

6
ティアダウンが適切に実行されていることを確認してください。そうでない場合、JUnitアプリケーションが終了できないため、Eclipseなどのランナーでテストが失敗します。:)
MetroidFan2002 2008年

6
セキュリティマネージャーを使用したソリューションが気に入らない。それをテストするためだけに私にとってハックのようです。
ニコライロイシュリング、2009年

6
junit 4.7以降を使用している場合は、ライブラリでSystem.exit呼び出しのキャプチャを処理してください。システムルール- stefanbirkner.github.com/system-rules
ウィル

6
「System.exit(whateverValue)で終了する代わりに、チェックされていない例外をスローしないのはなぜですか?」- System.exit無効なコマンドライン引数が指定されるたびに呼び出すコマンドライン引数処理フレームワークを使用しているためです。
Adam Parkin 2013

106

ライブラリのシステムルールには、ExpectedSystemExitと呼ばれるJUnit ルールがあります。このルールを使用すると、System.exit(...)を呼び出すコードをテストできます。

public void MyTest {
    @Rule
    public final ExpectedSystemExit exit = ExpectedSystemExit.none();

    @Test
    public void systemExitWithArbitraryStatusCode() {
        exit.expectSystemExit();
        //the code under test, which calls System.exit(...);
    }

    @Test
    public void systemExitWithSelectedStatusCode0() {
        exit.expectSystemExitWithStatus(0);
        //the code under test, which calls System.exit(0);
    }
}

完全な開示:私はそのライブラリの作者です。


3
完璧です。エレガント。元のコードの一部を変更したり、セキュリティマネージャーで遊んだりする必要はありませんでした。これが一番の答えになるはずです!
2012

2
これが一番の答えです。System.exitの適切な使用に関する無限の議論の代わりに、要点を説明します。さらに、JUnit自体に準拠した柔軟なソリューションであるため、ホイールを再発明したり、セキュリティマネージャーを混乱させる必要はありません。
L.ホランダ

@LeoHolandaこのソリューションは、私の回答への反対票を正当化するために使用したのと同じ「問題」の影響を受けませんか?また、TestNGユーザーはどうですか?
ホジェリオ

2
@LeoHolandaその通りです。ルールの実装を見ると、「システム終了」チェックの呼び出しが発生したときに例外をスローするカスタムセキュリティマネージャーを使用しているため、テストが終了します。私の回答に追加されたテスト例は、BTWの両方の要件(System.exitが呼び出される/呼び出されないことによる適切な検証)を満たしています。ExpectedSystemRuleもちろんの使用はいいです。問題は、追加のサードパーティライブラリが必要であることです。このライブラリは、実際の有用性に関してほとんど提供せず、JUnit固有です。
ホジェリオ

2
ありますexit.checkAssertionAfterwards()
ステファン・バークナー

31

このメソッドに「ExitManager」を挿入するのはどうですか。

public interface ExitManager {
    void exit(int exitCode);
}

public class ExitManagerImpl implements ExitManager {
    public void exit(int exitCode) {
        System.exit(exitCode);
    }
}

public class ExitManagerMock implements ExitManager {
    public bool exitWasCalled;
    public int exitCode;
    public void exit(int exitCode) {
        exitWasCalled = true;
        this.exitCode = exitCode;
    }
}

public class MethodsCallExit {
    public void CallsExit(ExitManager exitManager) {
        // whatever
        if (foo) {
            exitManager.exit(42);
        }
        // whatever
    }
}

製品コードはExitManagerImplを使用し、テストコードはExitManagerMockを使用して、exit()が呼び出されたかどうか、およびどの終了コードで呼び出されたかを確認できます。


1
+1素晴らしい解決策。また、ExitManagerが単純なコンポーネントになるため、Springを使用している場合は、実装も簡単です。ただし、exitManager.exit()の呼び出し後にコードが実行されないようにする必要があることに注意してください。コードがモックのExitManagerでテストされている場合、exitManager.exitを呼び出しても、コードは実際には終了しません。
Joman68

31

JUnitテストでは、実際にメソッドをモックまたはスタブ化できSystem.exitます。

たとえば、JMockitを使用して次のように書くことができます(他の方法もあります)。

@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
    // Called by code under test:
    System.exit(); // will not exit the program
}


編集:(最新のJMockit APIを使用した)代替テストでは、への呼び出し後にコードを実行できませんSystem.exit(n)

@Test(expected = EOFException.class)
public void checkingForSystemExitWhileNotAllowingCodeToContinueToRun() {
    new Expectations(System.class) {{ System.exit(anyInt); result = new EOFException(); }};

    // From the code under test:
    System.exit(1);
    System.out.println("This will never run (and not exit either)");
}

2
投票の理由:このソリューションの問題は、System.exitがコードの最後の行でない場合(つまり、if条件の内部)、コードが引き続き実行されることです。
L. Holanda

@LeoHolanda exit呼び出し後にコードが実行されないようにするテストのバージョンを追加しました(それが実際に問題であったわけではありません、IMO)。
ホジェリオ

最後のSystem.out.println()をassertステートメントで置き換える方が適切でしょうか?
user515655 2017

"@Mocked(" exit ")"は、JMockit 1.43では動作しないようです: "属性値は、モック化された注釈タイプに対して未定義です"ドキュメント: "モックフィールドを宣言するときに使用できる3つのモック化注釈があり、パラメータ:@Mocked、モックされたクラスのすべての既存および将来のインスタンスのすべてのメソッドとコンストラクタをモックします(それを使用するテストの期間中)。 " jmockit.github.io/tutorial/Mocking.html#mocked
ThorstenSchöning

実際にあなたの提案を試しましたか?"java.lang.IllegalArgumentException:Class java.lang.System is not mockable" RuntimeをモックできますがSystem.exit、少なくともGradleでテストを実行している私のシナリオでは停止しません。
ThorstenSchöning2018年

20

コードベースで使用したトリックの1つは、System.exit()への呼び出しをRunnable実装にカプセル化することでした。これは、問題のメソッドがデフォルトで使用します。ユニットテストでは、別のモックRunnableを設定します。このようなもの:

private static final Runnable DEFAULT_ACTION = new Runnable(){
  public void run(){
    System.exit(0);
  }
};

public void foo(){ 
  this.foo(DEFAULT_ACTION);
}

/* package-visible only for unit testing */
void foo(Runnable action){   
  // ...some stuff...   
  action.run(); 
}

...そしてJUnitテストメソッド...

public void testFoo(){   
  final AtomicBoolean actionWasCalled = new AtomicBoolean(false);   
  fooObject.foo(new Runnable(){
    public void run(){
      actionWasCalled.set(true);
    }   
  });   
  assertTrue(actionWasCalled.get()); 
}

これは依存性注入と呼ばれるものですか?
トーマスアーレ2010

2
この例の記述は、中途半端な依存関係注入の一種です。依存関係は(public fooメソッドまたは単体テストによって)パッケージの可視のfooメソッドに渡されますが、メインクラスはデフォルトのRunnable実装をハードコードします。
Scott Bale

6

System.exit()をラップするモック可能なクラスを作成します

EricSchaeferに同意します。しかし、Mockitoのような優れたモックフレームワークを使用する場合は、単純な具象クラスで十分であり、インターフェイスと2つの実装は必要ありません。

System.exit()でのテスト実行の停止

問題:

// do thing1
if(someCondition) {
    System.exit(1);
}
// do thing2
System.exit(0)

モックSytem.exit()は実行を終了しません。thing2実行されていないことをテストしたい場合、これは悪いことです。

解決:

martinの提案に従って、このコードをリファクタリングする必要があります。

// do thing1
if(someCondition) {
    return 1;
}
// do thing2
return 0;

そしてSystem.exit(status)、呼び出し関数で行います。これにより、すべてSystem.exit()のを1つの場所またはその近くに配置する必要がありmain()ます。これはSystem.exit()、ロジックの内部を呼び出すよりもクリーンです。

コード

ラッパー:

public class SystemExit {

    public void exit(int status) {
        System.exit(status);
    }
}

メイン:

public class Main {

    private final SystemExit systemExit;


    Main(SystemExit systemExit) {
        this.systemExit = systemExit;
    }


    public static void main(String[] args) {
        SystemExit aSystemExit = new SystemExit();
        Main main = new Main(aSystemExit);

        main.executeAndExit(args);
    }


    void executeAndExit(String[] args) {
        int status = execute(args);
        systemExit.exit(status);
    }


    private int execute(String[] args) {
        System.out.println("First argument:");
        if (args.length == 0) {
            return 1;
        }
        System.out.println(args[0]);
        return 0;
    }
}

テスト:

public class MainTest {

    private Main       main;

    private SystemExit systemExit;


    @Before
    public void setUp() {
        systemExit = mock(SystemExit.class);
        main = new Main(systemExit);
    }


    @Test
    public void executeCallsSystemExit() {
        String[] emptyArgs = {};

        // test
        main.executeAndExit(emptyArgs);

        verify(systemExit).exit(1);
    }
}

5

私はすでに与えられた回答のいくつかが好きですが、レガシーコードをテストするときにしばしば役立つ別のテクニックをデモンストレーションしたいと思いました。次のようなコードを考えます:

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      System.exit(i);
    }
  }
}

安全なリファクタリングを実行して、System.exit呼び出しをラップするメソッドを作成できます。

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      exit(i);
    }
  }

  void exit(int i) {
    System.exit(i);
  }
}

次に、exitをオーバーライドするテスト用の偽物を作成できます。

public class TestFoo extends TestCase {

  public void testShouldExitWithNegativeNumbers() {
    TestFoo foo = new TestFoo();
    foo.bar(-1);
    assertTrue(foo.exitCalled);
    assertEquals(-1, foo.exitValue);
  }

  private class TestFoo extends Foo {
    boolean exitCalled;
    int exitValue;
    void exit(int i) {
      exitCalled = true;
      exitValue = i;
    }
}

これは、テストケースの動作を置換するための一般的な手法であり、レガシーコードをリファクタリングするときに常に使用します。それは通常私が物事を残そうとしている場所ではなく、既存のコードをテストするための中間ステップです。


2
この方法では、exit()が呼び出されても、制御フローは停止しません。代わりに例外を使用してください。
Andrea Francia

3

APIをざっと見てみると、System.exitが例外espをスローできることがわかります。securitymanagerがvmのシャットダウンを禁止する場合。たぶん解決策は、そのようなマネージャーをインストールすることでしょう。


3

java SecurityManagerを使用して、現在のスレッドがJava VMをシャットダウンしないようにすることができます。次のコードはあなたが望むことをするはずです:

SecurityManager securityManager = new SecurityManager() {
    public void checkPermission(Permission permission) {
        if ("exitVM".equals(permission.getName())) {
            throw new SecurityException("System.exit attempted and blocked.");
        }
    }
};
System.setSecurityManager(securityManager);

うーん。System.exitのドキュメントは、名前が「exitVM」のcheckPermissionではなく、checkExit(int)が呼び出されることを明確に述べています。両方をオーバーライドする必要があるのでしょうか。
Chris Conway、

パーミッション名は実際にはexitVM。(statuscode)、つまりexitVM.0のようです-少なくとも私の最近のOSXでのテストでは。
Mark Derricutt

3

VonCの答えをJUnit 4で実行するために、コードを次のように変更しました

protected static class ExitException extends SecurityException {
    private static final long serialVersionUID = -1982617086752946683L;
    public final int status;

    public ExitException(int status) {
        super("There is no escape!");
        this.status = status;
    }
}

private static class NoExitSecurityManager extends SecurityManager {
    @Override
    public void checkPermission(Permission perm) {
        // allow anything.
    }

    @Override
    public void checkPermission(Permission perm, Object context) {
        // allow anything.
    }

    @Override
    public void checkExit(int status) {
        super.checkExit(status);
        throw new ExitException(status);
    }
}

private SecurityManager securityManager;

@Before
public void setUp() {
    securityManager = System.getSecurityManager();
    System.setSecurityManager(new NoExitSecurityManager());
}

@After
public void tearDown() {
    System.setSecurityManager(securityManager);
}

3

ランタイムインスタンスを置き換えることでSystem.exit(..)をテストできます。たとえば、TestNG + Mockitoの場合:

public class ConsoleTest {
    /** Original runtime. */
    private Runtime originalRuntime;

    /** Mocked runtime. */
    private Runtime spyRuntime;

    @BeforeMethod
    public void setUp() {
        originalRuntime = Runtime.getRuntime();
        spyRuntime = spy(originalRuntime);

        // Replace original runtime with a spy (via reflection).
        Utils.setField(Runtime.class, "currentRuntime", spyRuntime);
    }

    @AfterMethod
    public void tearDown() {
        // Recover original runtime.
        Utils.setField(Runtime.class, "currentRuntime", originalRuntime);
    }

    @Test
    public void testSystemExit() {
        // Or anything you want as an answer.
        doNothing().when(spyRuntime).exit(anyInt());

        System.exit(1);

        verify(spyRuntime).exit(1);
    }
}

2

返された終了コードが呼び出し側プログラムによって使用される環境があります(MS BatchのERRORLEVELなど)。コードでこれを行う主なメソッドの周りにテストがあり、私たちのアプローチは、他のテストで使用されているものと同様のSecurityManagerオーバーライドを使用することです。

昨夜、Junit @Ruleアノテーションを使用して小さなJARを作成し、セキュリティマネージャーコードを非表示にし、予想される戻りコードに基づいて予想を追加しました。http://code.google.com/p/junitsystemrules/


2

ほとんどのソリューションは

  • テストが終了する(メソッド全体、実行全体ではない)System.exit()と呼ばれる瞬間
  • すでにインストールされているものを無視する SecurityManager
  • テストフレームワークに固有の場合
  • テストケースごとに最大1回の使用に制限する

したがって、ほとんどのソリューションは次のような状況には適していません。

  • 副作用の検証は、 System.exit()
  • 既存のセキュリティマネージャはテストの一部です。
  • 別のテストフレームワークが使用されます。
  • 1つのテストケースで複数の検証を行いたい。これは厳密にはお勧めできませんが、assertAll()たとえばとの組み合わせでは特に便利な場合があります。

他の回答で提示された既存のソリューションによって課せられた制限に満足できなかったため、自分で何かを思い付きました。

次のクラスは、指定された値で呼び出されるassertExits(int expectedStatus, Executable executable)ことを表明するメソッドを提供し、テストはその後も続行できます。JUnit 5と同じように動作します。また、既存のセキュリティマネージャを尊重します。System.exit()statusassertThrows

残っている問題が1つあります。テスト対象のコードが新しいセキュリティマネージャーをインストールすると、テストによって設定されたセキュリティマネージャーが完全に置き換えられます。SecurityManager私が知っている他のすべてのベースのソリューションは同じ問題を抱えています。

import java.security.Permission;

import static java.lang.System.getSecurityManager;
import static java.lang.System.setSecurityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

public enum ExitAssertions {
    ;

    public static <E extends Throwable> void assertExits(final int expectedStatus, final ThrowingExecutable<E> executable) throws E {
        final SecurityManager originalSecurityManager = getSecurityManager();
        setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(final Permission perm) {
                if (originalSecurityManager != null)
                    originalSecurityManager.checkPermission(perm);
            }

            @Override
            public void checkPermission(final Permission perm, final Object context) {
                if (originalSecurityManager != null)
                    originalSecurityManager.checkPermission(perm, context);
            }

            @Override
            public void checkExit(final int status) {
                super.checkExit(status);
                throw new ExitException(status);
            }
        });
        try {
            executable.run();
            fail("Expected System.exit(" + expectedStatus + ") to be called, but it wasn't called.");
        } catch (final ExitException e) {
            assertEquals(expectedStatus, e.status, "Wrong System.exit() status.");
        } finally {
            setSecurityManager(originalSecurityManager);
        }
    }

    public interface ThrowingExecutable<E extends Throwable> {
        void run() throws E;
    }

    private static class ExitException extends SecurityException {
        final int status;

        private ExitException(final int status) {
            this.status = status;
        }
    }
}

次のようなクラスを使用できます。

    @Test
    void example() {
        assertExits(0, () -> System.exit(0)); // succeeds
        assertExits(1, () -> System.exit(1)); // succeeds
        assertExits(2, () -> System.exit(1)); // fails
    }

コードは、必要に応じてJUnit 4、TestNG、またはその他のフレームワークに簡単に移植できます。フレームワーク固有の要素は、テストに失敗したことだけです。これは、フレームワークに依存しないもの(Junit 4以外)に簡単に変更できます。 Rule

たとえば、assertExits()カスタマイズ可能なメッセージによるオーバーロードなど、改善の余地があります。


1

Runtime.exec(String command)別のプロセスでJVMを起動するために使用します。


3
単体テストとは別のプロセスにある場合、テスト中のクラスとどのようにやり取りしますか?
DNA

1
99%のケースで、System.exitはmainメソッド内にあります。したがって、コマンドライン引数(つまり、args)を使用してそれらを操作します。
L.ホランダ

1

SecurityManagerソリューションに軽微な問題があります。などの一部のメソッドJFrame.exitOnCloseもを呼び出しますSecurityManager.checkExit。私のアプリケーションでは、その呼び出しが失敗したくなかったので、

Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);

0

main。()内で行わない限り、System.exit()の呼び出しは悪い習慣です。これらのメソッドは、最終的にはmain()によってキャッチされた例外をスローし、適切なコードでSystem.exitを呼び出します。


8
しかし、それは質問の答えにはなりません。テストされる関数が最終的にメインメソッドである場合はどうなりますか?したがって、System.exit()の呼び出しは有効で、設計の観点からは問題ない可能性があります。そのためのテストケースをどのように書きますか?
エリー

3
mainメソッドは引数を取ってパーサーメソッドに渡し、アプリケーションをキックスタートするだけなので、mainメソッドをテストする必要はありません。テストするメインメソッドにロジックがあってはなりません。
Thomas Owens、

5
@エリー:これらのタイプの質問には、2つの有効な答えがあります。提起された質問に答える人と、質問の根拠を尋ねる人。両方のタイプの回答、特に両方を一緒にすると、理解が深まります。
runaros 2008
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.