Teste JUnit para System.out.println ()

370

Preciso escrever testes JUnit para um aplicativo antigo mal projetado e escrevendo muitas mensagens de erro na saída padrão. Quando o getResponse(String request)método se comporta corretamente, ele retorna uma resposta XML:

@BeforeClass
public static void setUpClass() throws Exception {
    Properties queries = loadPropertiesFile("requests.properties");
    Properties responses = loadPropertiesFile("responses.properties");
    instance = new ResponseGenerator(queries, responses);
}

@Test
public void testGetResponse() {
    String request = "<some>request</some>";
    String expResult = "<some>response</some>";
    String result = instance.getResponse(request);
    assertEquals(expResult, result);
}

Mas quando obtém XML malformado ou não entende a solicitação, ele retorna nulle grava algumas coisas na saída padrão.

Existe alguma maneira de afirmar a saída do console no JUnit? Para capturar casos como:

System.out.println("match found: " + strExpr);
System.out.println("xml not well formed: " + e.getMessage());
Mike Minicki
fonte
Relacionado a, mas não uma duplicata, de stackoverflow.com/questions/3381801/… #
6604

Respostas:

581

usar ByteArrayOutputStream e System.setXXX é simples:

private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
private final PrintStream originalOut = System.out;
private final PrintStream originalErr = System.err;

@Before
public void setUpStreams() {
    System.setOut(new PrintStream(outContent));
    System.setErr(new PrintStream(errContent));
}

@After
public void restoreStreams() {
    System.setOut(originalOut);
    System.setErr(originalErr);
}

casos de teste de amostra:

@Test
public void out() {
    System.out.print("hello");
    assertEquals("hello", outContent.toString());
}

@Test
public void err() {
    System.err.print("hello again");
    assertEquals("hello again", errContent.toString());
}

Eu usei esse código para testar a opção de linha de comando (afirmando que -version gera a string da versão, etc etc)

Editar: versões anteriores desta resposta são chamadas System.setOut(null)após os testes; Essa é a causa dos comentadores de NullPointerExceptions.

dfa
fonte
Além disso, usei o JUnitMatchers para testar as respostas: assertThat (result, containsString ("<request: GetEmployeeByKeyResponse")); Obrigado, dfa.
Mike Minicki
3
Eu prefiro usar System.setOut (null) para restaurar a volta fluxo ao que era quando a VM foi lançado
tddmonkey
5
Os javadocs não dizem nada sobre poder passar nulo para System.setOut ou System.setErr. Tem certeza de que isso funcionará em todos os JREs?
finnw
55
Encontrei um NullPointerExceptionem outros testes depois de definir um fluxo de erro nulo, conforme sugerido acima (em java.io.writer(Object), chamado internamente por um validador XML). Em vez disso, sugiro salvar o original em um campo: oldStdErr = System.erre restaurá-lo no @Aftermétodo
Luke Usherwood
6
Ótima solução. Apenas uma observação para quem o utiliza, pode ser necessário aparar () espaço em branco / nova linha de outContent.
Allison
102

Eu sei que este é um thread antigo, mas há uma boa biblioteca para fazer isso:

Regras do sistema

Exemplo dos documentos:

public void MyTest {
    @Rule
    public final SystemOutRule systemOutRule = new SystemOutRule().enableLog();

    @Test
    public void overrideProperty() {
        System.out.print("hello world");
        assertEquals("hello world", systemOutRule.getLog());
    }
}

Também permitirá interceptar System.exit(-1)e outras coisas pelas quais uma ferramenta de linha de comando precisaria ser testada.

Vai
fonte
11
Essa abordagem está repleta de problemas porque o fluxo de saída padrão é um recurso compartilhado usado por todas as partes do seu programa. É melhor usar a Injeção de Dependência para eliminar o uso direto do fluxo de saída padrão: stackoverflow.com/a/21216342/545127
Raedwald 29/18
30

Em vez de redirecionar System.out, refataria a classe que usa System.out.println()passando a PrintStreamcomo colaborador e depois usando System.outna produção e um Test Spy no teste. Ou seja, use Injeção de Dependência para eliminar o uso direto do fluxo de saída padrão.

Em produção

ConsoleWriter writer = new ConsoleWriter(System.out));

No teste

ByteArrayOutputStream outSpy = new ByteArrayOutputStream();
ConsoleWriter writer = new ConsoleWriter(new PrintStream(outSpy));
writer.printSomething();
assertThat(outSpy.toString(), is("expected output"));

Discussão

Dessa forma, a classe em teste se torna testável por uma refatoração simples, sem a necessidade de redirecionamento indireto da saída padrão ou interceptação obscura com uma regra do sistema.

user1909402
fonte
11
Não encontrei este ConsoleWriter em nenhum lugar do JDK: onde está?
Jean-Philippe Caruana 28/08
3
Provavelmente deve ser mencionado na resposta, mas acredito que a classe foi criada pelo usuário1909402.
Sebastian
6
Eu acho que ConsoleWriteré o assunto de teste,
Niel de Wet
22

Você pode definir o fluxo de impressão System.out através de setOut () (e para ine err). Você pode redirecionar isso para um fluxo de impressão que grava em uma sequência e depois inspecioná-lo? Esse parece ser o mecanismo mais simples.

(Eu recomendaria, em algum momento, converter o aplicativo em alguma estrutura de log - mas suspeito que você já esteja ciente disso!)

Brian Agnew
fonte
11
Isso foi algo que me veio à mente, mas eu não podia acreditar que não há uma maneira padrão da JUnit de fazer isso. Obrigado, cérebro. Mas os créditos chegaram ao dfa pelo esforço real.
Mike Minicki
Essa abordagem está repleta de problemas porque o fluxo de saída padrão é um recurso compartilhado usado por todas as partes do seu programa. É melhor usar a Injeção de Dependência para eliminar o uso direto do fluxo de saída padrão: stackoverflow.com/a/21216342/545127
Raedwald 29/18
Sim. Eu faria segundo que e talvez até mesmo questionar a assert logging (melhor afirmar uma chamada para um componente de log ou similar)
Brian Agnew
13

Um pouco fora do tópico, mas no caso de algumas pessoas (como eu, quando eu encontrei este tópico) estarem interessadas em capturar a saída de log via SLF4J, o JUnit do commons-testing@Rule pode ajudar:

public class FooTest {
    @Rule
    public final ExpectedLogs logs = new ExpectedLogs() {{
        captureFor(Foo.class, LogLevel.WARN);
    }};

    @Test
    public void barShouldLogWarning() {
        assertThat(logs.isEmpty(), is(true)); // Nothing captured yet.

        // Logic using the class you are capturing logs for:
        Foo foo = new Foo();
        assertThat(foo.bar(), is(not(nullValue())));

        // Assert content of the captured logs:
        assertThat(logs.isEmpty(), is(false));
        assertThat(logs.contains("Your warning message here"), is(true));
    }
}

Isenção de responsabilidade :

  • Desenvolvi esta biblioteca, pois não consegui encontrar nenhuma solução adequada para minhas próprias necessidades.
  • Somente ligações para log4j, log4j2e logbackestão disponíveis no momento, mas estou feliz para adicionar mais.
Marc Carré
fonte
Muito obrigado por criar esta biblioteca! Eu tenho procurado algo assim por um longo tempo! É muito, muito útil, pois às vezes você simplesmente não pode simplificar seu código o suficiente para ser facilmente testável, mas com uma mensagem de log, você pode fazer maravilhas!
carlspring
Isso parece realmente promissor ... mas mesmo quando eu apenas copio seu programa ATMTest e o executo como um teste no Gradle, estou recebendo uma exceção ... Eu levantei um problema na sua página do Github ...
mike roent
9

A resposta do @dfa é ótima, então dei um passo adiante para tornar possível testar blocos de saída.

Primeiro eu criei TestHelpercom um método captureOutputque aceita a classe chata CaptureTest. O método captureOutput faz o trabalho de definir e derrubar os fluxos de saída. Quando a execução CaptureOutputdo testmétodo é chamado, ele tem acesso à produção gerar para o bloco de teste.

Origem do TestHelper:

public class TestHelper {

    public static void captureOutput( CaptureTest test ) throws Exception {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        ByteArrayOutputStream errContent = new ByteArrayOutputStream();

        System.setOut(new PrintStream(outContent));
        System.setErr(new PrintStream(errContent));

        test.test( outContent, errContent );

        System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
        System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.out)));

    }
}

abstract class CaptureTest {
    public abstract void test( ByteArrayOutputStream outContent, ByteArrayOutputStream errContent ) throws Exception;
}

Observe que TestHelper e CaptureTest estão definidos no mesmo arquivo.

Em seu teste, você pode importar o captureOutput estático. Aqui está um exemplo usando o JUnit:

// imports for junit
import static package.to.TestHelper.*;

public class SimpleTest {

    @Test
    public void testOutput() throws Exception {

        captureOutput( new CaptureTest() {
            @Override
            public void test(ByteArrayOutputStream outContent, ByteArrayOutputStream errContent) throws Exception {

                // code that writes to System.out

                assertEquals( "the expected output\n", outContent.toString() );
            }
        });
}
mguymon
fonte
7

Se você estava usando o Spring Boot (você mencionou que está trabalhando com um aplicativo antigo, provavelmente não é, mas pode ser útil para outras pessoas), use org.springframework.boot.test.rule.OutputCapture da seguinte maneira:

@Rule
public OutputCapture outputCapture = new OutputCapture();

@Test
public void out() {
    System.out.print("hello");
    assertEquals(outputCapture.toString(), "hello");
}
Disper
fonte
11
Votei sua resposta com voto positivo porque uso a bota Spring e ela me colocou no caminho certo. Obrigado! No entanto, o outputCapture precisa ser inicializado. (public OutputCapture outputCapture = new OutputCapture ();) Consulte docs.spring.io/spring-boot/docs/current/reference/html/…
EricGreg 25/17
Você está absolutamente correto. Obrigado pelo comentário! Eu atualizei minha resposta.
Disper
4

Com base na resposta do @ dfa e outra resposta que mostra como testar o System.in , gostaria de compartilhar minha solução para fornecer uma entrada para um programa e testar sua saída.

Como referência, eu uso o JUnit 4.12.

Digamos que temos este programa que simplesmente replica entrada para saída:

import java.util.Scanner;

public class SimpleProgram {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print(scanner.next());
        scanner.close();
    }
}

Para testá-lo, podemos usar a seguinte classe:

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;

public class SimpleProgramTest {
    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    @Before
    public void setUpOutput() {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data) {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput() {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput() {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    @Test
    public void testCase1() {
        final String testString = "Hello!";
        provideInput(testString);

        SimpleProgram.main(new String[0]);

        assertEquals(testString, getOutput());
    }
}

Não vou explicar muita coisa, porque acredito que o código é legível e citei minhas fontes.

Quando o JUnit é executado testCase1(), ele chama os métodos auxiliares na ordem em que aparecem:

  1. setUpOutput(), por causa da @Beforeanotação
  2. provideInput(String data), chamado de testCase1()
  3. getOutput(), chamado de testCase1()
  4. restoreSystemInputOutput(), por causa da @Afteranotação

Não testei System.errporque não precisava, mas deve ser fácil de implementar, semelhante ao teste System.out.

Antonio Vinicius Menezes Medei
fonte
1

Você não deseja redirecionar o fluxo system.out porque ele redireciona para a JVM INTEIRA. Qualquer outra coisa em execução na JVM pode ser confusa. Existem melhores maneiras de testar a entrada / saída. Olhe para stubs / zombarias.

Sam Jacobs
fonte
1

Exemplo completo do JUnit 5 para testar System.out(substitua a parte when):

package learning;

import static org.assertj.core.api.BDDAssertions.then;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class SystemOutLT {

    private PrintStream originalSystemOut;
    private ByteArrayOutputStream systemOutContent;

    @BeforeEach
    void redirectSystemOutStream() {

        originalSystemOut = System.out;

        // given
        systemOutContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(systemOutContent));
    }

    @AfterEach
    void restoreSystemOutStream() {
        System.setOut(originalSystemOut);
    }

    @Test
    void shouldPrintToSystemOut() {

        // when
        System.out.println("example");

        then(systemOutContent.toString()).containsIgnoringCase("example");
    }
}
Jens Piegsa
fonte
0

Você não pode imprimir diretamente usando system.out.println ou usando a API do logger enquanto estiver usando JUnit . Mas se você quiser verificar algum valor, basta usar

Assert.assertEquals("value", str);

Irá lançar abaixo o erro de afirmação:

java.lang.AssertionError: expected [21.92] but found [value]

Seu valor deve ser 21,92. Agora, se você testar usando esse valor, como abaixo, seu caso de teste será aprovado.

 Assert.assertEquals(21.92, str);
Affy
fonte
0

pra fora

@Test
void it_prints_out() {

    PrintStream save_out=System.out;final ByteArrayOutputStream out = new ByteArrayOutputStream();System.setOut(new PrintStream(out));

    System.out.println("Hello World!");
    assertEquals("Hello World!\r\n", out.toString());

    System.setOut(save_out);
}

por erro

@Test
void it_prints_err() {

    PrintStream save_err=System.err;final ByteArrayOutputStream err= new ByteArrayOutputStream();System.setErr(new PrintStream(err));

    System.err.println("Hello World!");
    assertEquals("Hello World!\r\n", err.toString());

    System.setErr(save_err);
}
Shimon Doodkin
fonte
Para esse tipo de lógica de configuração e desmontagem, eu usaria um @Rule, em vez de fazê-lo em linha em seu teste. Notavelmente, se sua afirmação falhar, a segunda System.setOut/Errchamada não será alcançada.
Dimo414 11/0718