System.out.println() 的 JUnit 测试

Posted

技术标签:

【中文标题】System.out.println() 的 JUnit 测试【英文标题】:JUnit test for System.out.println() 【发布时间】:2010-11-10 07:14:31 【问题描述】:

我需要为一个设计不佳并且正在向标准输出写入大量错误消息的旧应用程序编写 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());

【问题讨论】:

与***.com/questions/3381801/…相关但不重复 【参考方案1】:

使用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(" 我更喜欢使用 System.setOut(null) 将流恢复到启动 VM 时的状态 javadocs 没有说明能够将 null 传递给 System.setOut 或 System.setErr。您确定这适用于所有 JRE 吗? 在按照上面的建议设置空错误流后,我在其他测试中遇到了NullPointerException(在java.io.writer(Object) 中,由 XML 验证器内部调用)。我建议将原件保存在一个字段中:oldStdErr = System.err 并在 @After 方法中恢复它。 很好的解决方案。只是给任何使用它的人的注释,您可能需要从 outContent 中修剪()空格/换行符。【参考方案2】:

我知道这是一个旧线程,但是有一个不错的库可以做到这一点:System Rules 文档中的示例:

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) 和其他需要测试命令行工具的内容。

【讨论】:

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

我将重构使用System.out.println() 的类,而不是重定向System.out,方法是将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"));

讨论

通过这种方式,被测类通过简单的重构变得可测试,无需间接重定向标准输出或使用系统规则进行模糊拦截。

【讨论】:

我在 JDK 的任何地方都找不到这个 ConsoleWriter:它在哪里? 答案中可能应该提到,但我相信该类是由 user1909402 创建的。 我认为ConsoleWriter是测试对象,【参考方案4】:

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

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

【讨论】:

这是我想到的,但我不敢相信没有标准的 JUnit 方法可以做到这一点。谢谢,大脑。但实际工作的功劳归于 dfa。 这种方法充满了问题,因为标准输出流是程序所有部分都使用的共享资源。最好使用依赖注入来消除直接使用标准输出流:***.com/a/21216342/545127 是的。我会支持这一点,甚至可能会质疑日志记录断言(最好断言对日志记录组件或类似组件的调用)【参考方案5】:

有点离题,但如果有些人(比如我,当我第一次发现这个线程时)可能对通过 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));
    

免责声明

我开发了这个库,因为我找不到任何适合我自己需求的解决方案。 目前只有log4jlog4j2logback 的绑定可用,但我很乐意添加更多。

【讨论】:

非常感谢您创建这个库!我一直在寻找这样的东西这么久!它非常非常有用,因为有时您根本无法将代码简化到足以轻松测试的程度,但是通过日志消息您可以创造奇迹! 这看起来很有希望...但是即使我只是复制您的 ATMTest 程序并在 Gradle 下将其作为测试运行,我也遇到了异常...我在您的网站上提出了一个问题Github 页面...【参考方案6】:

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

首先,我使用一个方法captureOutput 创建了TestHelper,该方法接受了令人讨厌的类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");

【讨论】:

我对您的回答投了赞成票,因为我使用 Spring Boot,它让我走上了正确的道路。谢谢!但是,需要初始化 outputCapture。 (public OutputCapture outputCapture = new OutputCapture();) 见docs.spring.io/spring-boot/docs/current/reference/html/… 你是绝对正确的。感谢您的评论!我已经更新了我的答案。【参考方案8】:

基于@dfa's answer 和another answer that shows how to test 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() 时,它将按照帮助方法出现的顺序调用它们:

    setUpOutput(),因为 @Before 注释 provideInput(String data),来自testCase1() getOutput(),来自testCase1() restoreSystemInputOutput(),因为 @After 注释

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

【讨论】:

【参考方案9】:

完整的 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");
    

【讨论】:

【参考方案10】:

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

【讨论】:

【参考方案11】:

您不能在使用 JUnit 时使用 system.out.println 或使用 logger api 直接打印。但是,如果您想检查任何值,那么您只需使用

Assert.assertEquals("value", str);

它会抛出以下断言错误:

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

你的值应该是 21.92,现在如果你像下面这样使用这个值进行测试,你的测试用例就会通过。

 Assert.assertEquals(21.92, str);

【讨论】:

【参考方案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!\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 调用将不会到达。【参考方案13】:

如果函数正在打印到 System.out,您可以通过使用 System.setOut 方法将 System.out 更改为您提供的 PrintStream 来捕获该输出。如果您创建一个连接到 ByteArrayOutputStream 的 PrintStream,那么您可以将输出捕获为字符串。

// Create a stream to hold the output
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
// IMPORTANT: Save the old System.out!
PrintStream old = System.out;
// Tell Java to use your special stream
System.setOut(ps);
// Print some output: goes to your special stream
System.out.println("Foofoofoo!");
// Put things back
System.out.flush();
System.setOut(old);
// Show what happened
System.out.println("Here: " + baos.toString());

【讨论】:

【参考方案14】:

虽然这个问题已经很老了,并且已经有了很好的答案,但我想提供一个替代方案。我喜欢dfa 的答案,但是我想在不复制配置的情况下在不同的项目中重复使用一些东西,所以我用它创建了一个库并想回馈社区。它被称为Console Captor,您可以使用以下 sn-p 添加它:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>consolecaptor</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

示例类

public class FooService 

    public void sayHello() 
        System.out.println("Keyboard not responding. Press any key to continue...");
        System.err.println("Congratulations, you are pregnant!");
    


单元测试

import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.console.ConsoleCaptor;
import org.junit.jupiter.api.Test;

public class FooServiceTest 

    @Test
    public void captureStandardAndErrorOutput() 
        ConsoleCaptor consoleCaptor = new ConsoleCaptor();

        FooService fooService = new FooService();
        fooService.sayHello();

        assertThat(consoleCaptor.getStandardOutput()).contains("Keyboard not responding. Press any key to continue...");
        assertThat(consoleCaptor.getErrorOutput()).contains("Congratulations, you are pregnant!");
        
        consoleCaptor.close();
    

【讨论】:

以上是关于System.out.println() 的 JUnit 测试的主要内容,如果未能解决你的问题,请参考以下文章

System.out.println 和 System.err.println 乱序

Scala 中的 println 与 System.out.println

JAVA中System.out.println和System.out.print有什么区别

java中的system.out.println()和JSP中out.println()差别

JAVA中System.out.println和System.out.print有啥区别?

java的System. out. println(),为啥out能调用println()?