关于java:System.out.println()的JUnit测试

JUnit test for System.out.println()

我需要为一个设计很差的旧应用程序编写JUnit测试,并将大量错误消息写入标准输出。 当getResponse(String request)方法行为正确时,它返回XML响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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中断言控制台输出? 要抓住像这样的案例:

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


使用ByteArrayOutputStream和System.setXXX很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}

样本测试用例:

1
2
3
4
5
6
7
8
9
10
11
@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评论者引用的原因。


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

系统规则

来自文档的示例:

1
2
3
4
5
6
7
8
9
10
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)以及命令行工具需要测试的其他内容。


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

在生产中

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

在测试中

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

讨论

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


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

(我会主张,在某个阶段,将应用程序转换为一些日志框架 - 但我怀疑你已经意识到了这一点!)


稍微偏离主题,但是如果有些人(像我这样,当我第一次找到这个帖子时)可能对通过SLF4J捕获日志输出感兴趣,那么commons-testing的JUnit @Rule可能会有所帮助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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));
    }
}

免责声明:

  • 我开发了这个库,因为我找不到适合自己需要的解决方案。
  • 目前只提供log4jlog4j2logback的绑定,但我很乐意添加更多内容。


@dfa答案很棒,所以我更进一步,以便测试输出块。

首先,我用方法captureOutput创建了TestHelper,接受了烦人的类CaptureTest。 captureOutput方法执行设置和拆除输出流的工作。当调用captureOutputtest方法的实现时,它可以访问测试块的输出生成。

TestHelper的来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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
"
, outContent.toString() );
            }
        });
}


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

1
2
3
4
5
6
7
8
@Rule
public OutputCapture outputCapture = new OutputCapture();

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


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

作为参考,我使用JUnit 4.12。

假设我们有这个程序简单地将输入复制到输出:

1
2
3
4
5
6
7
8
9
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();
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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()时,它将按照它们出现的顺序调用辅助方法:

  • setUpOutput(),因为@Before注释
  • provideInput(String data),从testCase1()调用
  • getOutput(),从testCase1()调用
  • restoreSystemInputOutput(),因为@After注释
  • 我没有测试System.err,因为我不需要它,但它应该很容易实现,类似于测试System.out


    完整的JUnit 5示例测试System.out(替换when部分):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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");
        }
    }

    您不希望重定向system.out流,因为它重定向到整个JVM。在JVM上运行的任何其他东西都可能搞砸了。有更好的方法来测试输入/输出。查看存根/模拟。


    出去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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!

    "
    , out.toString());

        System.setOut(save_out);
    }

    为了错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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!

    "
    , err.toString());

        System.setErr(save_err);
    }


    使用JUnit时,无法使用system.out.println或使用logger api直接打印。但是如果你想检查任何值,那么你就可以使用了

    1
    Assert.assertEquals("value", str);

    它将抛出以下断言错误:

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

    你的值应该是21.92,现在如果你将测试使用这个值,如下所示,你的测试用例将通过。

    1
     Assert.assertEquals(21.92, str);