225

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

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

4

19 に答える 19

236

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

  • なぜSystem.exit()ですか?

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

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

  • 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+)ルールのコレクションであるシステムルールを使用してコメントで提案します。 これは、2011年12月の回答でStefanBirknerによって最初に言及されました。java.lang.System

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);
    }
}
于 2008-11-21T16:48:22.920 に答える
127

ライブラリSystemLambdaにはメソッドがありcatchSystemExitます。このルールを使用すると、System.exit(...)を呼び出すコードをテストできます。

public class MyTest {
    @Test
    public void systemExitWithArbitraryStatusCode() {
        SystemLambda.catchSystemExit(() -> {
            //the code under test, which calls System.exit(...);
        });
    }


    @Test
    public void systemExitWithSelectedStatusCode0() {
        int status = SystemLambda.catchSystemExit(() -> {
            //the code under test, which calls System.exit(0);
        });

        assertEquals(0, status);
    }
}

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

public class 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);
    }
}

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

于 2011-12-28T16:25:40.190 に答える
32

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)");
}
于 2009-07-26T18:27:49.360 に答える
32

このメソッドに「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() が呼び出されたかどうか、およびどの終了コードで終了したかを確認できます。

于 2008-11-21T16:59:19.967 に答える
21

コードベースで使用したトリックの1つは、System.exit()の呼び出しをRunnable implにカプセル化することでした。これは、問題のメソッドがデフォルトで使用します。単体テストでは、別のモック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()); 
}
于 2008-11-21T16:50:49.547 に答える
5

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

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);
}
于 2010-01-31T17:45:23.687 に答える
5

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);
    }
}
于 2011-12-29T15:36:02.843 に答える
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;
    }
}

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

于 2008-11-21T18:31:29.157 に答える
3

APIをざっと見てみると、System.exitが例外espをスローする可能性があることがわかります。セキュリティマネージャがVMのシャットダウンを禁止している場合。たぶん解決策は、そのようなマネージャーをインストールすることでしょう。

于 2008-11-21T16:45:07.610 に答える
3

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

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);
于 2008-11-21T16:55:13.297 に答える
3

System.exit(..) をテストして、Runtime インスタンスを置き換えることができます。たとえば、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);
    }
}
于 2013-06-11T19:51:55.527 に答える
2

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

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

于 2010-06-22T20:08:21.357 に答える
1

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

于 2010-10-20T11:28:48.757 に答える
1

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

Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
于 2012-03-27T23:49:01.423 に答える
1

単体テストと統合テストに使用できる一般的に有用なアプローチは、 run() および exit() メソッドを提供するパッケージ プライベート (デフォルト アクセス) のモック可能なランナー クラスを使用することです。これらのメソッドは、テスト モジュールの Mock または Fake テスト クラスによってオーバーライドできます。

テスト クラス (JUnit またはその他) は、exit() メソッドが System.exit() の代わりにスローできる例外を提供します。

package mainmocked;
class MainRunner {
    void run(final String[] args) {
        new MainMocked().run(args);    
    }
    void exit(final int status) {
        System.exit(status);
    }
}

以下の main() を含むクラスには、ユニットまたは統合テスト時にモックまたは偽のランナーを受け取る altMain() もあります。

package mainmocked;

public class MainMocked {
    private static MainRunner runner = new MainRunner();

    static void altMain(final String[] args, final MainRunner inRunner) {
        runner = inRunner;
        main(args);
    }

    public static void main(String[] args) {
        try {
          runner.run(args);
        } catch (Throwable ex) {
            // Log("error: ", ex);
            runner.exit(1);
        }
        runner.exit(0);
    } // main


    public void run(String[] args) {
        // do things ...
    }
} // class

シンプルなモック (Mockito を使用) は次のようになります。

@Test
public void testAltMain() {
    String[] args0 = {};
    MainRunner mockRunner = mock(MainRunner.class);
    MainMocked.altMain(args0, mockRunner);

    verify(mockRunner).run(args0);
    verify(mockRunner).exit(0);
  }

より複雑なテスト クラスでは、run() で何でもできる Fake と、System.exit() を置き換える Exception クラスを使用します。

private class FakeRunnerRuns extends MainRunner {
    @Override
    void run(String[] args){
        new MainMocked().run(args);
    }
    @Override
    void exit(final int status) {
        if (status == 0) {
            throw new MyMockExitExceptionOK("exit(0) success");
        }
        else {
            throw new MyMockExitExceptionFail("Unexpected Exception");
        } // ok
    } // exit
} // class
于 2021-07-09T22:03:35.373 に答える
-1

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

于 2008-11-21T16:50:42.277 に答える