用于System.out.println()的JUnit测试


370

我需要为设计欠佳的旧应用程序编写JUnit测试,并将旧的错误消息写入标准输出。该getResponse(String request)方法的行为正确时,将返回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);
}

但是,当XML格式错误或无法理解请求时,它将返回null并将某些内容写入标准输出。

有什么方法可以在JUnit中声明控制台输出?要捕获类似的情况:

System.out.println("match found: " + strExpr);
System.out.println("xml not well formed: " + e.getMessage());

Answers:


581

使用ByteArrayOutputStream和System.setXXX很简单:

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

样本测试案例:

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

我使用此代码测试了命令行选项(断言-version输出版本字符串等)

编辑:System.setOut(null)测试后 调用此答案的先前版本;这是NullPointerExceptions评论者引用的原因。


此外,我已经使用JUnitMatchers来测试响应:assertThat(result,containsString(“ <request:GetEmployeeByKeyResponse”))); 谢谢,dfa。
Mike Minicki 09年

3
我更喜欢使用System.setOut(null)将流还原到启动虚拟机时的状态
tddmonkey

5
Javadocs并没有说有关能够将null传递给System.setOut或System.setErr的任何内容。您确定这对所有JRE都适用吗?
finnw

55
NullPointerException在按照上述建议设置空错误流后,我在其他测试中遇到了(在中java.io.writer(Object),由XML验证器内部调用)。我建议改为将原件保存在字段中:oldStdErr = System.err并在@After方法中还原它。
路加·乌舍伍德

6
很好的解决方案。只是对使用它的任何人的注释,您可能需要从outContent修剪()空格/换行符。
艾里森

101

我知道这是一个旧线程,但是有一个不错的库可以做到这一点:

系统规则

来自文档的示例:

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

它还将允许您捕获System.exit(-1)命令行工具需要测试的其他内容。


1
这种方法充满了问题,因为标准输出流是程序所有部分使用的共享资源。最好使用依赖注入来消除对标准输出流的直接使用: stackoverflow.com/a/21216342/545127
Raedwald,

30

System.out我将System.out.println()通过传递a PrintStream作为协作者,然后System.out在生产环境中使用和在测试中使用Test Spy来重构使用的类,而不是进行重定向。也就是说,使用依赖注入可以消除对标准输出流的直接使用。

在生产中

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

在测试中

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

讨论区

这样,通过简单的重构就可以测试被测类,而无需间接重定向标准输出或使用系统规则来模糊拦截。


1
我在JDK的任何地方都找不到此ConsoleWriter:它在哪里?
让-菲利普·卡鲁阿娜

3
在答案中可能应该提到它,但是我相信该类是由user1909402创建的。
塞巴斯蒂安

6
我认为这ConsoleWriter是测试主题,
Niel de Wet

22

您可以通过setOut()(以及for inerr)设置System.out打印流。您可以将其重定向到记录为字符串的打印流,然后进行检查吗?这似乎是最简单的机制。

(我建议在某个阶段将应用程序转换为某些日志记录框架-但我怀疑您已经意识到了这一点!)


1
那是我想到的,但是我不敢相信没有标准的JUnit方法可以做到这一点。谢谢,大脑。但是功劳确实归功于dfa。
Mike Minicki 09年

这种方法充满了问题,因为标准输出流是程序所有部分使用的共享资源。最好使用依赖注入来消除对标准输出流的直接使用: stackoverflow.com/a/21216342/545127
Raedwald,

是。我要第二个论点,甚至可能会质疑日志记录断言(更好地断言对日志记录组件或类似组件的调用)
Brian Agnew

13

稍微偏离主题,但是如果某些人(例如我,当我第一次发现此线程)可能对通过SLF4J捕获日志输出感兴趣时,commons-testing的JUnit @Rule可能会有所帮助:

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

免责声明

  • 我开发此库是因为找不到适合自己需求的解决方案。
  • 仅用于绑定log4jlog4j2并且logback都可以在瞬间,但我很高兴加入更多。

非常感谢您创建此库!很长一段时间以来,我一直在寻找这样的东西!这非常非常有用,因为有时您根本无法简化代码以使其易于测试,但是通过日志消息您可以创造奇迹!
carlspring

这看起来确实很有希望...但是即使当我只是复制您的ATMTest程序并在Gradle下作为测试运行时,我也遇到了异常...我在您的Github页面上提出了一个问题...
Mike Rodent

9

@dfa的答案很好,因此我更进一步,可以测试输出块了。

首先,我创建TestHelper了一个captureOutput接受类的方法CaptureTest。captureOutput方法完成设置和分解输出流的工作。调用CaptureOutputtest方法的实现时,它可以访问为测试块生成的输出。

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

请注意,TestHelper和CaptureTest是在同一文件中定义的。

然后在测试中,您可以导入静态captureOutput。这是使用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() );
            }
        });
}

7

如果您使用的是Spring Boot(您提到过您正在使用旧的应用程序,那么您可能没有用,但可能对其他人有用),那么您可以使用org.springframework.boot.test.rule.OutputCapture以以下方式:

@Rule
public OutputCapture outputCapture = new OutputCapture();

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

1
我赞成您的回答,因为我使用Spring Boot,它使我走上了正确的轨道。谢谢!但是,outputCapture需要初始化。(公共OutputCapture outputCapture = new OutputCapture();)请参阅docs.spring.io/spring-boot/docs/current/reference/html/…–
EricGreg

你是绝对正确的。感谢您的评论!我已经更新了答案。
Disper

4

基于@dfa的答案另一个答案,该答案显示了如何测试System.in,我想共享我的解决方案以为程序提供输入并测试其输出。

作为参考,我使用JUnit 4.12。

假设我们有一个程序,该程序将输入复制到输出:

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

要测试它,我们可以使用以下类:

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

我不会解释太多,因为我相信代码是可读的并且引用了我的资料。

当JUnit运行时testCase1(),它将按它们出现的顺序调用帮助程序方法:

  1. setUpOutput(),因为有@Before注释
  2. provideInput(String data),来自 testCase1()
  3. getOutput(),来自 testCase1()
  4. restoreSystemInputOutput(),因为有@After注释

我没有进行测试System.err是因为我不需要它,但是它应该易于实现,类似于test System.out


1

您不想重定向system.out流,因为这将重定向整个IREJVM。在JVM上运行的其他任何东西都可能被弄乱。有更好的方法来测试输入/输出。查看存根/模拟。


1

完整的JUnit 5示例进行测试System.out(替换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");
    }
}

0

使用JUnit时,不能使用system.out.printlnlogger api直接打印。但是,如果您想检查任何值,则只需使用

Assert.assertEquals("value", str);

它将抛出以下断言错误:

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

您的值应为21.92,现在,如果您将使用此值进行测试,例如下面的测试用例将通过。

 Assert.assertEquals(21.92, str);

0

为了

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

对于错误

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

对于这种设置和拆卸逻辑,我将使用@Rule,而不是在测试中内联。值得注意的是,如果您的断言失败,System.setOut/Err不会到达第二个电话。
dimo414 '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.