Test JUnit pour System.out.println ()

370

J'ai besoin d'écrire des tests JUnit pour une ancienne application mal conçue et écrit beaucoup de messages d'erreur sur la sortie standard. Lorsque la getResponse(String request)méthode se comporte correctement, elle renvoie une réponse 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);
}

Mais quand il obtient du XML mal formé ou ne comprend pas la demande, il retourne nullet écrit des trucs sur la sortie standard.

Existe-t-il un moyen d'affirmer la sortie de la console dans JUnit? Pour attraper des cas comme:

System.out.println("match found: " + strExpr);
System.out.println("xml not well formed: " + e.getMessage());
Mike Minicki
la source
Lié à, mais pas un doublon de stackoverflow.com/questions/3381801/…
Raedwald

Réponses:

581

l'utilisation de ByteArrayOutputStream et System.setXXX est simple:

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

exemples de cas de test:

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

J'ai utilisé ce code pour tester l'option de ligne de commande (en affirmant que -version génère la chaîne de version, etc., etc.)

Edit: versions antérieures de cette réponse appelées System.setOut(null)après les tests; C'est la cause des commentaires de NullPointerExceptions.

dfa
la source
De plus, j'ai utilisé JUnitMatchers pour tester les réponses: assertThat (result, containsString ("<request: GetEmployeeByKeyResponse")); Merci, dfa.
Mike Minicki
3
Je préfère utiliser System.setOut (null) pour restaurer le flux à ce qu'il était lorsque la machine virtuelle a été lancée
tddmonkey
5
Les javadocs ne disent rien sur la possibilité de passer null à System.setOut ou System.setErr. Êtes-vous sûr que cela fonctionnera sur tous les JRE?
finnw
55
J'ai rencontré un NullPointerExceptiondans d'autres tests après avoir défini un flux d'erreur nul comme suggéré ci-dessus (dans java.io.writer(Object), appelé en interne par un validateur XML). Je suggère plutôt de sauvegarder l'original dans un champ: oldStdErr = System.erret de le restaurer dans la @Afterméthode.
Luke Usherwood
6
Excellente solution. Juste une note pour toute personne l'utilisant, vous devrez peut-être couper () les espaces / les nouvelles lignes de outContent.
Allison
102

Je sais que c'est un vieux fil, mais il y a une belle bibliothèque pour le faire:

Règles système

Exemple de la documentation:

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

Il vous permettra également d'intercepter System.exit(-1)et d'autres choses pour lesquelles un outil de ligne de commande devrait être testé.

Volonté
la source
1
Cette approche est lourde de problèmes car le flux de sortie standard est une ressource partagée utilisée par toutes les parties de votre programme. Il est préférable d'utiliser l'injection de dépendance pour éliminer l'utilisation directe du flux de sortie standard: stackoverflow.com/a/21216342/545127
Raedwald
30

Au lieu de rediriger System.out, je refactoriserais la classe qui utilise System.out.println()en passant a PrintStreamcomme collaborateur puis en utilisant System.outen production et un Test Spy dans le test. Autrement dit, utilisez l'injection de dépendance pour éliminer l'utilisation directe du flux de sortie standard.

En production

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

Dans le test

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

Discussion

De cette façon, la classe testée peut être testée par une simple refactorisation, sans avoir besoin de redirection indirecte de la sortie standard ou d'interception obscure avec une règle système.

user1909402
la source
1
Je n'ai pas pu trouver ce ConsoleWriter n'importe où dans le JDK: où est-il?
Jean-Philippe Caruana
3
Cela devrait probablement être mentionné dans la réponse, mais je pense que cette classe a été créée par user1909402.
Sebastian
6
Je pense que ConsoleWriterc'est le sujet du test,
Niel de Wet
22

Vous pouvez définir le flux d'impression System.out via setOut () (et pour inet err). Pouvez-vous rediriger cela vers un flux d'impression qui enregistre dans une chaîne, puis inspecter cela? Cela semblerait être le mécanisme le plus simple.

(Je recommanderais, à un moment donné, de convertir l'application en un cadre de journalisation - mais je soupçonne que vous en êtes déjà conscient!)

Brian Agnew
la source
1
C'est quelque chose qui m'est venu à l'esprit, mais je ne pouvais pas croire qu'il n'y ait pas de méthode standard JUnit pour le faire. Merci, Brain. Mais les crédits sont arrivés à dfa pour l'effort réel.
Mike Minicki
Cette approche est lourde de problèmes car le flux de sortie standard est une ressource partagée utilisée par toutes les parties de votre programme. Il est préférable d'utiliser l'injection de dépendance pour éliminer l'utilisation directe du flux de sortie standard: stackoverflow.com/a/21216342/545127
Raedwald
Oui. J'appuierais cela et remettrais peut-être même en question une assertion de journalisation (mieux vaut affirmer un appel sur un composant de journalisation ou similaire)
Brian Agnew
13

Un peu hors sujet, mais dans le cas où certaines personnes (comme moi, quand je trouvé ce fil) pourraient être intéressés à capturer la sortie du journal via SLF4J, commons-test de » JUnit @Rulepourrait l'aide:

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

Clause de non - responsabilité :

  • J'ai développé cette bibliothèque car je n'ai pas trouvé de solution adaptée à mes besoins.
  • Seules les fixations pour log4j, log4j2et logbacksont disponibles pour le moment, mais je suis heureux d'en ajouter d'autres.
Marc Carré
la source
Merci beaucoup d'avoir créé cette bibliothèque! Je cherchais quelque chose comme ça depuis si longtemps! C'est très, très utile car parfois vous ne pouvez tout simplement pas simplifier suffisamment votre code pour être facilement testable, mais avec un message de log, vous pouvez faire des merveilles!
carlspring
Cela semble vraiment prometteur ... mais même lorsque je copie simplement votre programme ATMTest et l'exécute en tant que test sous Gradle, je reçois une exception ... J'ai soulevé un problème sur votre page Github ...
Mike Rodent
9

La réponse @dfa est excellente, j'ai donc fait un pas de plus pour permettre de tester des blocs de sortie.

J'ai d'abord créé TestHelperavec une méthode captureOutputqui accepte la classe ennuyeuse CaptureTest. La méthode captureOutput effectue le travail de définition et de suppression des flux de sortie. Lorsque l'implémentation de CaptureOutputla testméthode de est appelée, elle a accès à la sortie générée pour le bloc de test.

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

Notez que TestHelper et CaptureTest sont définis dans le même fichier.

Ensuite, dans votre test, vous pouvez importer le captureOutput statique. Voici un exemple utilisant 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
la source
7

Si vous utilisiez Spring Boot (vous avez mentionné que vous travaillez avec une ancienne application, vous ne l'êtes probablement pas, mais elle pourrait être utile à d'autres), alors vous pourriez utiliser org.springframework.boot.test.rule.OutputCapture De la manière suivante:

@Rule
public OutputCapture outputCapture = new OutputCapture();

@Test
public void out() {
    System.out.print("hello");
    assertEquals(outputCapture.toString(), "hello");
}
Disper
la source
1
J'ai voté pour votre réponse parce que j'utilise Spring Boot et cela m'a mis sur la bonne voie. Merci! Cependant, outputCapture doit être initialisé. (public OutputCapture outputCapture = new OutputCapture ();) Voir docs.spring.io/spring-boot/docs/current/reference/html/…
EricGreg
Vous avez absolument raison. Merci pour le commentaire! J'ai mis à jour ma réponse.
Disper
4

Sur la base de la réponse de @ dfa et d' une autre réponse qui montre comment tester System.in , je voudrais partager ma solution pour donner une entrée à un programme et tester sa sortie.

Comme référence, j'utilise JUnit 4.12.

Disons que nous avons ce programme qui réplique simplement l'entrée en sortie:

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

Pour le tester, nous pouvons utiliser la classe suivante:

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

Je n'expliquerai pas grand-chose, car je pense que le code est lisible et j'ai cité mes sources.

Lorsque JUnit s'exécute testCase1(), il va appeler les méthodes d'assistance dans l'ordre où elles apparaissent:

  1. setUpOutput(), à cause de l' @Beforeannotation
  2. provideInput(String data), appelé depuis testCase1()
  3. getOutput(), appelé depuis testCase1()
  4. restoreSystemInputOutput(), à cause de l' @Afterannotation

Je n'ai pas testé System.errparce que je n'en avais pas besoin, mais il devrait être facile à implémenter, similaire aux tests System.out.

Antonio Vinicius Menezes Medei
la source
1

Vous ne voulez pas rediriger le flux system.out car cela redirige pour la JVM ENTIÈRE. Tout autre élément exécuté sur la JVM peut être gâché. Il existe de meilleures façons de tester les entrées / sorties. Examinez les moignons / moqueries.

Sam Jacobs
la source
1

Exemple complet de JUnit 5 à tester System.out(remplacez la partie quand):

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
la source
0

Vous ne pouvez pas imprimer directement à l'aide de system.out.println ou à l'aide de l' API de l'enregistreur lorsque vous utilisez JUnit . Mais si vous voulez vérifier des valeurs, vous pouvez simplement utiliser

Assert.assertEquals("value", str);

Il renverra l'erreur d'assertion ci-dessous:

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

Votre valeur devrait être 21,92, maintenant si vous testez en utilisant cette valeur comme ci-dessous, votre cas de test passera.

 Assert.assertEquals(21.92, str);
Affy
la source
0

pour sortir

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

pour erreur

@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
la source
Pour ce type de logique de configuration et de démontage, j'utiliserais un @Rule, plutôt que de le faire en ligne dans votre test. Notamment, si votre assertion échoue, le deuxième System.setOut/Errappel ne sera pas atteint.
dimo414