Test JUnit dla System.out.println ()

370

Muszę napisać testy JUnit dla starej aplikacji, która jest źle zaprojektowana i pisze wiele komunikatów o błędach na standardowe wyjście. Gdy getResponse(String request)metoda zachowuje się poprawnie, zwraca odpowiedź 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);
}

Ale kiedy otrzyma zniekształcony XML lub nie zrozumie żądania, zwraca nulli zapisuje pewne rzeczy na standardowym wyjściu.

Czy jest jakiś sposób na potwierdzenie wyjścia konsoli w JUnit? Aby złapać przypadki takie jak:

System.out.println("match found: " + strExpr);
System.out.println("xml not well formed: " + e.getMessage());
Mike Minicki
źródło
Powiązane, ale nie duplikat stackoverflow.com/questions/3381801/…
Raedwald

Odpowiedzi:

581

korzystanie z ByteArrayOutputStream i System.setXXX jest proste:

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

przykładowe przypadki testowe:

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

Użyłem tego kodu do przetestowania opcji wiersza poleceń (stwierdzenie, że -version wyświetla ciąg wersji itp.)

Edycja: wcześniejsze wersje tej odpowiedzi wywoływane System.setOut(null)po testach; To jest przyczyna, do której odwołują się komentatorzy NullPointerExceptions.

dfa
źródło
Ponadto użyłem JUnitMatchers do testowania odpowiedzi: assertThat (wynik, zawieraString („<żądanie: GetEmployeeByKeyResponse”)); Dzięki, dfa.
Mike Minicki
3
Wolę użyć System.setOut (null), aby przywrócić strumień z powrotem do stanu, w którym był uruchomiony VM
tddmonkey
5
Javadocs nie mówią nic o możliwości przekazania wartości null do System.setOut lub System.setErr. Czy na pewno to zadziała na wszystkich środowiskach JRE?
finnw
55
NullPointerExceptionW innych testach napotkałem po ustawieniu strumienia błędów o wartości zerowej, jak sugerowano powyżej (in java.io.writer(Object), wywoływany wewnętrznie przez moduł sprawdzania poprawności XML). Zamiast tego sugerowałbym zapisanie oryginału w polu: oldStdErr = System.erri przywrócenie tego w @Aftermetodzie.
Luke Usherwood
6
Świetne rozwiązanie. Tylko uwaga dla każdego, kto go używa, może być konieczne przycięcie () białych znaków / nowej linii z outContent.
Allison
101

Wiem, że to stary wątek, ale jest do tego przyjemna biblioteka:

Zasady systemowe

Przykład z dokumentów:

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

Pozwoli również na przechwytywanie System.exit(-1)i inne rzeczy, dla których narzędzie wiersza poleceń musiałoby zostać przetestowane.

Będzie
źródło
1
Takie podejście jest obarczone problemami, ponieważ standardowy strumień wyjściowy jest zasobem współdzielonym używanym przez wszystkie części twojego programu. Lepiej jest użyć Dependency Injection, aby wyeliminować bezpośrednie użycie standardowego strumienia wyjściowego: stackoverflow.com/a/21216342/545127
Raedwald,
30

Zamiast przekierowywać System.out, przefakturowałbym klasę, która używa System.out.println(), przekazując a PrintStreamjako współpracownika, a następnie używając System.outw produkcji i Test Spy w teście. To znaczy, użyj Dependency Injection, aby wyeliminować bezpośrednie użycie standardowego strumienia wyjściowego.

W produkcji

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

W teście

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

Dyskusja

W ten sposób testowana klasa staje się testowalna przez proste refaktoryzowanie, bez potrzeby pośredniego przekierowywania standardowego wyjścia lub niejasnego przechwytywania za pomocą reguły systemowej.

użytkownik1909402
źródło
1
Nie mogłem znaleźć tego ConsoleWriter nigdzie w JDK: gdzie on jest?
Jean-Philippe Caruana
3
Prawdopodobnie powinno to zostać wspomniane w odpowiedzi, ale uważam, że klasa została stworzona przez użytkownika user1909402.
Sebastian
6
Myślę, że ConsoleWriterjest to temat testowy,
Niel de Wet
22

Możesz ustawić strumień wydruku System.out za pomocą setOut () (oraz dla ini err). Czy możesz przekierować to do strumienia wydruku, który zapisuje ciąg, a następnie to sprawdzić? To wydaje się być najprostszym mechanizmem.

(Na pewnym etapie zalecałbym konwersję aplikacji do struktury rejestrowania - ale podejrzewam, że już o tym wiesz!)

Brian Agnew
źródło
1
To było coś, co przyszło mi do głowy, ale nie mogłem uwierzyć, że nie ma standardowego sposobu JUnit. Dzięki, mózg. Ale kredyty trafiły do ​​dfa za faktyczny wysiłek.
Mike Minicki
Takie podejście jest obarczone problemami, ponieważ standardowy strumień wyjściowy jest zasobem współdzielonym używanym przez wszystkie części twojego programu. Lepiej jest użyć Dependency Injection, aby wyeliminować bezpośrednie użycie standardowego strumienia wyjściowego: stackoverflow.com/a/21216342/545127
Raedwald,
Tak. Chciałbym poprzeć to, a może nawet zakwestionować asercję dotyczącą logowania (lepiej, aby wywołać wywołanie komponentu rejestrującego lub podobnego)
Brian Agnew,
13

Nie na temat, ale na wypadek, gdyby niektóre osoby (takie jak ja, kiedy pierwszy raz znalazłem ten wątek) mogły być zainteresowane przechwytywaniem danych wyjściowych dziennika za pośrednictwem SLF4J, JUnit do testowania wspólnych@Rule może pomóc:

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

Zastrzeżenie :

  • Opracowałem tę bibliotekę, ponieważ nie mogłem znaleźć odpowiedniego rozwiązania na własne potrzeby.
  • Tylko dla wiązania log4j, log4j2i logbacksą dostępne w tej chwili, ale jestem szczęśliwy, aby dodać więcej.
Marc Carré
źródło
Dziękuję bardzo za utworzenie tej biblioteki! Tak długo szukałem czegoś takiego! Jest to bardzo, bardzo przydatne, ponieważ czasami po prostu nie można uprościć kodu na tyle, aby można go było łatwo przetestować, ale za pomocą komunikatu dziennika można robić cuda!
Carlspring
Wygląda to naprawdę obiecująco ... ale nawet jeśli po prostu skopiuję twój program ATMTest i uruchomię go jako test pod Gradle, dostaję wyjątek ... Podniosłem problem na twojej stronie Github ...
Mike Rodent
9

Odpowiedź @dfa jest świetna, więc zrobiłem krok dalej, aby umożliwić testowanie bloków wyjścia.

Najpierw stworzyłem TestHelpermetodą, captureOutputktóra akceptuje annoymiczną klasę CaptureTest. Metoda captureOutput polega na ustawianiu i niszczeniu strumieni wyjściowych. Jeśli realizacja CaptureOutput„s testmetoda jest wywoływana, to ma dostęp do wyjścia wygeneruje dla bloku testowego.

Źródło dla 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;
}

Zauważ, że TestHelper i CaptureTest są zdefiniowane w tym samym pliku.

Następnie w teście możesz zaimportować static captureOutput. Oto przykład z użyciem 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
źródło
7

Jeśli korzystasz z Spring Boot (wspomniałeś, że pracujesz ze starą aplikacją, więc prawdopodobnie nie jesteś, ale może być przydatny dla innych), możesz użyć org.springframework.boot.test.rule.OutputCapture W następujący sposób:

@Rule
public OutputCapture outputCapture = new OutputCapture();

@Test
public void out() {
    System.out.print("hello");
    assertEquals(outputCapture.toString(), "hello");
}
Rozproszyć
źródło
1
Podniosłem głos twojej odpowiedzi, ponieważ używam wiosennego rozruchu i ustawiło mnie to na właściwej drodze. Dzięki! Jednak outputCapture musi zostać zainicjowany. (public OutputCapture outputCapture = new OutputCapture ();) Patrz docs.spring.io/spring-boot/docs/current/reference/html/…
EricGreg
Masz absolutną rację. Dziękuję za komentarz! Zaktualizowałem swoją odpowiedź.
Disper
4

W oparciu o odpowiedź @ dfa i inną odpowiedź, która pokazuje, jak przetestować System.in , chciałbym udostępnić moje rozwiązanie, aby przekazać dane wejściowe do programu i przetestować jego wyniki.

Jako odniesienie używam JUnit 4.12.

Powiedzmy, że mamy ten program, który po prostu replikuje dane wejściowe na dane wyjściowe:

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

Aby to przetestować, możemy użyć następującej klasy:

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

Nie wyjaśnię wiele, ponieważ uważam, że kod jest czytelny i zacytowałem swoje źródła.

Po uruchomieniu JUnit testCase1()będzie wywoływał metody pomocnicze w kolejności, w jakiej się pojawiają:

  1. setUpOutput(), z powodu @Beforeadnotacji
  2. provideInput(String data), wywoływany z testCase1()
  3. getOutput(), wywoływany z testCase1()
  4. restoreSystemInputOutput(), z powodu @Afteradnotacji

Nie testowałem, System.errponieważ go nie potrzebowałem, ale powinien być łatwy do wdrożenia, podobnie jak testowanie System.out.

Antonio Vinicius Menezes Medei
źródło
1

Nie chcesz przekierowywać strumienia system.out, ponieważ przekierowuje to dla CAŁEGO JVM. Cokolwiek innego działającego na JVM może zostać pomieszane. Istnieją lepsze sposoby testowania wejścia / wyjścia. Spójrz na kody pośredniczące / kpiny.

Sam Jacobs
źródło
1

Pełny przykład JUnit 5 do przetestowania System.out(wymień część 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
źródło
0

Nie można drukować bezpośrednio przy użyciu system.out.println lub interfejsu API rejestratora podczas korzystania z JUnit . Ale jeśli chcesz sprawdzić jakieś wartości, możesz po prostu użyć

Assert.assertEquals("value", str);

Wyrzuci poniżej błędu asercji:

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

Twoja wartość powinna wynosić 21,92. Teraz, jeśli będziesz testować przy użyciu tej wartości, jak poniżej, Twój test przejdzie.

 Assert.assertEquals(21.92, str);
Affy
źródło
0

na zewnątrz

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

dla błędu

@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
źródło
Do tego rodzaju konfiguracji i logiki porzucenia użyłbym @Rulezamiast, zamiast robić to bezpośrednio w teście. W szczególności, jeśli twoje twierdzenie nie powiedzie się, drugie System.setOut/Errpołączenie nie zostanie osiągnięte.
dimo414,