特定の入力でSystem.exit()
を呼び出すメソッドがいくつかあります。残念ながら、これらのケースをテストすると、JUnitが終了します! System.exit()
は現在のスレッドだけでなく、JVMを終了するため、メソッドコールを新しいスレッドに入れることは役に立たないようです。これに対処するための一般的なパターンはありますか?たとえば、System.exit()
のスタブを置き換えることはできますか?
[編集]問題のクラスは、実際にはJUnit内でテストしようとしているコマンドラインツールです。たぶん、JUnitは仕事に適したツールではないのでしょうか?補完的な回帰テストツールの提案を歓迎します(JUnitおよびEclEmmaとうまく統合できるものが望ましい)。
確かに、 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月の更新:
Will 提案 コメント内 使用 System Rules 、JUnit(4.9+)ルールのコレクションJava.lang.System
を使用するテストコード。
最初に言及されたのは、Stefan Birknerin his answer 2011年12月。
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);
}
}
ライブラリ システムルール には、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);
}
}
完全な開示:私はそのライブラリの著者です。
実際には、JUnitテストでSystem.exit
メソッドをモックまたはスタブアウトcanできます。
たとえば、 JMockit を使用すると、次のように記述できます(他の方法もあります)。
@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
// Called by code under test:
System.exit(); // will not exit the program
}
EDIT:System.exit(n)
の呼び出し後にコードの実行を許可しない代替テスト(最新のJMockit APIを使用)。
@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)");
}
このメソッドに「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つは、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());
}
EricSchaefer に同意します。しかし、 Mockito のような優れたモックフレームワークを使用する場合、単純な具象クラスで十分であり、インターフェイスと2つの実装は必要ありません。
問題:
// 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()
がmain()
内またはその近くに1か所に配置されます。これは、ロジック内部で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);
}
}
私はすでに与えられた答えのいくつかが好きですが、テスト対象のレガシーコードを取得する際にしばしば役立つ別のテクニックを示したかったのです。次のようなコードがあります:
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;
}
}
これは、テストケースの動作を置き換えるための一般的な手法であり、レガシーコードをリファクタリングするときに常に使用します。通常、私が何かを残す場所ではなく、既存のコードをテストするための中間ステップです。
APIをざっと見てみると、System.exitが例外espをスローできることがわかります。 securitymanagerがvmのシャットダウンを禁止している場合。たぶん、解決策はそのようなマネージャーをインストールすることでしょう。
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);
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);
}
ランタイムインスタンスを置き換えて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);
}
}
返された終了コードが呼び出し側プログラムによって使用される環境があります(MS BatchのERRORLEVELなど)。コード内でこれを行うメインメソッドの周りにテストがあり、ここでのその他のテストで使用されるのと同様のSecurityManagerオーバーライドを使用するアプローチがとられています。
昨夜、Junit @Ruleアノテーションを使用して小さなJARを作成し、セキュリティマネージャーコードを非表示にし、予想されるリターンコードに基づいて予想を追加しました。 http://code.google.com/p/junitsystemrules/
Runtime.exec(String command)
を使用して、別のプロセスでJVMを起動します。
SecurityManager
ソリューションには小さな問題があります。 JFrame.exitOnClose
などの一部のメソッドは、SecurityManager.checkExit
も呼び出します。私のアプリケーションでは、その呼び出しを失敗させたくなかったので、
Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
System.exit()を呼び出すことは、main()内で行わない限り、悪い習慣です。これらのメソッドは例外をスローする必要があり、最終的にはmain()がキャッチします。main()は適切なコードでSystem.exitを呼び出します。