Java: Como testar métodos que chamam System.exit ()?

198

Eu tenho alguns métodos que devem chamar System.exit()certas entradas. Infelizmente, testar esses casos faz com que o JUnit seja encerrado! Colocar as chamadas de método em um novo Thread não parece ajudar, pois System.exit()finaliza a JVM, não apenas o segmento atual. Existem padrões comuns para lidar com isso? Por exemplo, posso substituir um stub porSystem.exit() ?

[EDIT] A classe em questão é na verdade uma ferramenta de linha de comando que estou tentando testar dentro do JUnit. Talvez o JUnit simplesmente não seja a ferramenta certa para o trabalho? Sugestões para ferramentas complementares de teste de regressão são bem-vindas (de preferência algo que se integre bem ao JUnit e EclEmma).

Chris Conway
fonte
2
Estou curioso para saber por uma função jamais iria chamar System.exit () ...
Thomas Owens
2
Se você está chamando uma função que sai do aplicativo. Por exemplo, se o usuário tentar executar uma tarefa que não está autorizada a executar mais que x vezes seguidas, você será forçado a sair do aplicativo.
Elie
5
Ainda acho que, nesse caso, deve haver uma maneira mais agradável de retornar do aplicativo em vez de System.exit ().
21411 Thomas
27
Se você estiver testando main (), faz todo o sentido chamar System.exit (). Temos o requisito de que, por erro, um processo em lote saia com 1 e com êxito, com 0.
Matthew Farwell
5
Não concordo com todo mundo que diz que System.exit()é ruim - seu programa deve falhar rapidamente. Lançar uma exceção simplesmente prolonga um aplicativo em um estado inválido em situações em que um desenvolvedor deseja sair e gera erros falsos.
Sridhar Sarnobat

Respostas:

216

De fato, Derkeiler.com sugere:

  • Por quê System.exit() ?

Em vez de terminar com System.exit (WhateverValue), por que não lançar uma exceção desmarcada? Em uso normal, ele será direcionado para o apanhador de última hora da JVM e encerrará seu script (a menos que você decida capturá-lo em algum lugar ao longo do caminho, o que pode ser útil algum dia).

No cenário JUnit, ele será capturado pela estrutura JUnit, que relatará que esse teste falhou e se move suavemente para o próximo.

  • Evite System.exit()realmente sair da JVM:

Tente modificar o TestCase para executar com um gerenciador de segurança que evite chamar System.exit e, em seguida, pegue a 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);
        }
    }
}

Atualização em dezembro de 2012:

Will propõe nos comentários, usando as Regras do sistema , uma coleção de regras JUnit (4.9+) para testar o código usado java.lang.System.
Isso foi mencionado inicialmente por Stefan Birkner em sua resposta em dezembro de 2011.

System.exit(…)

Use a ExpectedSystemExitregra para verificar se isso System.exit(…)é chamado.
Você também pode verificar o status da saída.

Por exemplo:

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);
    }
}
VonC
fonte
4
Não gostei da primeira resposta, mas a segunda é bem legal - eu não tinha mexido com os gerentes de segurança e achava que eles eram mais complicados do que isso. No entanto, como você testa o gerenciador de segurança / mecanismo de teste.
Bill K
6
Certifique-se de que a desmontagem seja executada corretamente, caso contrário, seu teste falhará em um corredor como o Eclipse, porque o aplicativo JUnit não pode sair! :)
MetroidFan2002
6
Não gosto da solução usando o gerenciador de segurança. Parece um truque para mim apenas para testá-lo.
Nicolai Reuschling
6
Se você estiver usando o junit 4.7 ou superior, permita que uma biblioteca lide com a captura de suas chamadas System.exit. Regras sistema - stefanbirkner.github.com/system-rules
Will
6
"Em vez de terminar com System.exit (WhateverValue), por que não lançar uma exceção não verificada?" - porque estou usando a estrutura de processamento de argumentos da linha de comando que chama System.exitsempre que um argumento inválido da linha de comando é fornecido.
quer
107

A biblioteca System Rules possui uma regra JUnit chamada ExpectedSystemExit. Com esta regra, você pode testar o código, que chama 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);
    }
}

Divulgação completa: sou o autor dessa biblioteca.

Stefan Birkner
fonte
3
Perfeito. Elegante. Não foi necessário alterar um ponto do meu código original ou brincar com o gerente de segurança. Essa deve ser a melhor resposta!
Will
2
Essa deve ser a resposta principal. Em vez de um debate interminável sobre o uso adequado do System.exit, vá direto ao ponto. Além disso, uma solução flexível em conformidade com a própria JUnit, para que não seja necessário reinventar a roda ou atrapalhar o gerenciador de segurança.
L. Holanda
@LeoHolanda Esta solução não sofre do mesmo "problema" que você usou para justificar um voto negativo na minha resposta? Além disso, e os usuários do TestNG?
Rogério
2
@LeoHolanda Você está certo; olhando para a implementação da regra, agora vejo que ele usa um gerenciador de segurança personalizado que lança uma exceção quando ocorre a chamada para a verificação de "saída do sistema", encerrando o teste. O teste de exemplo adicionado na minha resposta atende a ambos os requisitos (verificação adequada com System.exit sendo chamado / não chamado), BTW. O uso de ExpectedSystemRuleé bom, é claro; o problema é que ela requer uma biblioteca extra de terceiros que fornece muito pouco em termos de utilidade no mundo real e é específica da JUnit.
Rogério
2
Existe exit.checkAssertionAfterwards().
precisa saber é o seguinte
31

Que tal injetar um "ExitManager" nesses métodos:

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
    }
}

O código de produção usa o ExitManagerImpl e o código de teste usa ExitManagerMock e pode verificar se exit () foi chamado e com qual código de saída.

EricSchaefer
fonte
1
+1 boa solução. Também é fácil de implementar se você estiver usando o Spring, pois o ExitManager se torna um componente simples. Lembre-se de que você precisa garantir que seu código não continue executando após a chamada exitManager.exit (). Quando seu código está sendo testado com um ExitManager simulado, o código não sai realmente após a chamada para exitManager.exit.
precisa saber é o seguinte
31

Na verdade, você pode zombar ou apagar oSystem.exit método em um teste JUnit.

Por exemplo, usando o JMockit, você pode escrever (existem outras maneiras também):

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


EDIT: Teste alternativo (usando a API JMockit mais recente) que não permite que nenhum código seja executado após uma chamada para 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)");
}
Rogério
fonte
2
Razão da votação: O problema com esta solução é que, se System.exit não for a última linha do código (ou seja, dentro da condição if), o código continuará sendo executado.
L. Holanda
@LeoHolanda Adicionada uma versão do teste que impede que o código seja executado após a exitchamada (não que realmente tenha sido um problema, IMO).
Rogério
Pode ser mais apropriado substituir o último System.out.println () por uma declaração assert?
user515655
"@Mocked (" exit ")" parece não funcionar mais com o JMockit 1.43: "O valor do atributo é indefinido para o tipo de anotação Mocked" Os documentos: "Existem três anotações de simulação diferentes que podemos usar ao declarar campos de simulação e parâmetros: @Mocked, que zomba de todos os métodos e construtores em todas as instâncias existentes e futuras de uma classe zombada (durante a duração dos testes que a utilizam); " jmockit.github.io/tutorial/Mocking.html#mocked
Thorsten Schöning
Você realmente tentou sua sugestão? Eu recebo: "java.lang.IllegalArgumentException: a classe java.lang.System não Runtimepode ser ridicularizada " pode ser ridicularizada, mas não para, System.exitpelo menos no meu cenário, na execução de testes em Gradle.
Thorsten Schöning
20

Um truque que usamos em nossa base de código foi fazer com que a chamada para System.exit () fosse encapsulada em um impl Runnable, que o método em questão usou por padrão. Para teste de unidade, definimos um Runnable simulado diferente. Algo assim:

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(); 
}

... e o método de teste JUnit ...

public void testFoo(){   
  final AtomicBoolean actionWasCalled = new AtomicBoolean(false);   
  fooObject.foo(new Runnable(){
    public void run(){
      actionWasCalled.set(true);
    }   
  });   
  assertTrue(actionWasCalled.get()); 
}
Scott Bale
fonte
É isso que eles chamam de injeção de dependência?
Thomas Ahle
2
Este exemplo, como escrito, é uma espécie de injeção de dependência incompleta - a dependência é passada para o método foo visível no pacote (pelo método public foo ou pelo teste de unidade), mas a classe principal ainda codifica a implementação Runnable padrão.
Scott Bale
6

Crie uma classe com capacidade de simulação que envolva System.exit ()

Eu concordo com EricSchaefer . Mas se você usar uma boa estrutura de zombaria como o Mockito , basta uma classe simples e concreta, sem necessidade de uma interface e duas implementações.

Parando a execução de teste em System.exit ()

Problema:

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

Uma zombaria Sytem.exit()não terminará a execução. Isso é ruim se você quiser testar issothing2 se não é executado.

Solução:

Você deve refatorar esse código conforme sugerido por martin :

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

E faça System.exit(status)na função de chamada. Isso força você a ter todos os seus System.exit()s em um só lugar ou perto dele main(). Isso é mais limpo do que chamarSystem.exit() profundamente dentro de sua lógica.

Código

Embrulho:

public class SystemExit {

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

A Principal:

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;
    }
}

Teste:

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);
    }
}
Arend v. Reinersdorff
fonte
5

Gosto de algumas das respostas já dadas, mas queria demonstrar uma técnica diferente que geralmente é útil ao testar o código legado. Código fornecido como:

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

Você pode fazer uma refatoração segura para criar um método que agrupe a chamada System.exit:

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

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

Em seguida, você pode criar um falso para o seu teste que substitui a saída:

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;
    }
}

Essa é uma técnica genérica para substituir o comportamento por casos de teste, e eu a uso o tempo todo ao refatorar o código legado. Geralmente não é onde eu vou deixar as coisas, mas uma etapa intermediária para testar o código existente.

Jeffrey Fredrick
fonte
2
Esta técnica não interrompe o fluxo de controle quando a saída () é chamada. Use uma exceção em seu lugar.
Andrea Francia
3

Uma rápida olhada na API, mostra que o System.exit pode gerar uma exceção esp. se um gerenciador de segurança proíbe o desligamento da VM. Talvez uma solução seria instalar um gerente desse tipo.

flolo
fonte
3

Você pode usar o java SecurityManager para impedir que o encadeamento atual desligue a Java VM. O código a seguir deve fazer o que você deseja:

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);
Marc Novakowski
fonte
Hum. Os documentos System.exit dizem especificamente que checkExit (int) será chamado, não checkPermission com nome = "exitVM". Eu me pergunto se devo substituir os dois?
21430 Chris Conway
O nome da permissão realmente parece ser exitVM. (Statuscode), ou seja, exitVM.0 - pelo menos no meu teste recente no OSX.
Mark Derricutt
3

Para que a resposta do VonC seja executada no JUnit 4, modifiquei o código da seguinte maneira

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);
}
Jeow Li Huan
fonte
3

Você pode testar System.exit (..) substituindo a instância do Runtime. Por exemplo, com 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);
    }
}
ursa
fonte
2

Existem ambientes em que o código de saída retornado é usado pelo programa de chamada (como ERRORLEVEL no MS Batch). Temos testes em torno dos métodos principais que fazem isso em nosso código, e nossa abordagem foi usar uma substituição semelhante do SecurityManager, como usado em outros testes aqui.

Ontem à noite, montei um pequeno JAR usando as anotações do Junit @Rule para ocultar o código do gerenciador de segurança, além de adicionar expectativas com base no código de retorno esperado. http://code.google.com/p/junitsystemrules/

Dan Watt
fonte
2

A maioria das soluções

  • finalizar o teste (método, não a execução inteira), o momento System.exit()é chamado
  • ignore um já instalado SecurityManager
  • Às vezes, seja bastante específico para uma estrutura de teste
  • restrito a ser usado no máximo uma vez por caso de teste

Portanto, a maioria das soluções não é adequada para situações em que:

  • A verificação dos efeitos colaterais deve ser realizada após a chamada para System.exit()
  • Um gerenciador de segurança existente faz parte do teste.
  • Uma estrutura de teste diferente é usada.
  • Você deseja ter várias verificações em um único caso de teste. Isso pode não ser estritamente recomendado, mas pode ser muito conveniente às vezes, especialmente em combinação com assertAll(), por exemplo.

Não fiquei satisfeito com as restrições impostas pelas soluções existentes apresentadas nas outras respostas e, portanto, criei algo sozinho.

A classe a seguir fornece um método assertExits(int expectedStatus, Executable executable)que afirma que System.exit()é chamado com um statusvalor especificado e o teste pode continuar após ele. Funciona da mesma maneira que o JUnit 5 assertThrows. Ele também respeita um gerenciador de segurança existente.

Há um problema restante: quando o código em teste instala um novo gerenciador de segurança que substitui completamente o gerenciador de segurança definido pelo teste. Todas as outras SecurityManagersoluções baseadas em mim conhecidas sofrem o mesmo problema.

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;
        }
    }
}

Você pode usar a classe assim:

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

O código pode ser facilmente portado para JUnit 4, TestNG ou qualquer outra estrutura, se necessário. O único elemento específico da estrutura está falhando no teste. Isso pode ser facilmente alterado para algo independente de estrutura (exceto um Junit 4 Rule

Há espaço para melhorias, por exemplo, sobrecarregando assertExits()com mensagens personalizáveis.

Christian Hujer
fonte
1

Use Runtime.exec(String command)para iniciar a JVM em um processo separado.

Alexei
fonte
3
Como você interagirá com a classe em teste, se ela estiver em um processo separado do teste de unidade?
DNA
1
Em 99% dos casos, o System.exit está dentro de um método principal. Portanto, você interage com eles por argumentos de linha de comando (ou seja, args).
L. Holanda
1

Há um pequeno problema com a SecurityManagersolução. Alguns métodos, como JFrame.exitOnClose, também chamam SecurityManager.checkExit. No meu aplicativo, eu não queria que a chamada falhasse, então usei

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

Chamar System.exit () é uma prática ruim, a menos que seja feita dentro de um main (). Esses métodos devem gerar uma exceção que, em última análise, é capturada pelo seu main (), que chama System.exit com o código apropriado.


fonte
8
Isso não responde à pergunta, no entanto. E se a função que está sendo testada for o método principal? Portanto, chamar System.exit () pode ser válido e aprovado da perspectiva do design. Como você escreve um caso de teste para ele?
Elie
3
Você não deve ter que testar o método principal, pois o método principal deve receber apenas argumentos, passá-los para um método analisador e, em seguida, iniciar o aplicativo. Não deve haver lógica no método principal a ser testado.
21411 Thomas
5
@ Ellie: Neste tipo de perguntas, existem duas respostas válidas. Um respondendo à pergunta colocada e um perguntando por que a pergunta foi baseada. Ambos os tipos de respostas proporcionam uma melhor compreensão e, especialmente, os dois juntos.
runaros 02/12/08