ทดสอบ JUnit สำหรับ System.out.println ()


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

เกี่ยวข้องกับ แต่ไม่ซ้ำกับstackoverflow.com/questions/3381801/…
Raedwald

คำตอบ:


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

ฉันใช้รหัสนี้เพื่อทดสอบตัวเลือกบรรทัดคำสั่ง (ยืนยันว่า - รุ่นส่งออกสตริงรุ่น ฯลฯ ฯลฯ )

แก้ไข: รุ่นก่อนหน้าของคำตอบนี้เรียกว่าSystem.setOut(null)หลังจากการทดสอบ; นี่คือสาเหตุของการแสดงความคิดเห็น NullPointerExceptions อ้างถึง


อีกต่อไปฉันใช้ JUnitMatchers เพื่อทดสอบการตอบกลับ: assertThat (ผลลัพธ์, containString ("<request: GetEmployeeByKeyResponse")); ขอบคุณ dfa
Mike Minicki

3
ฉันชอบที่จะใช้ System.setOut (null) เพื่อคืนค่ากระแสข้อมูลกลับไปเป็นเหมือนเดิมเมื่อเปิดตัว VM
tddmonkey

5
javadocs ไม่ได้พูดอะไรเกี่ยวกับความสามารถในการส่งผ่านโมฆะไปที่ System.setOut หรือ System.setErr คุณแน่ใจหรือว่าจะใช้กับ JRE ทั้งหมดได้
finnw

55
ฉันพบNullPointerExceptionในการทดสอบอื่นหลังจากตั้งค่าสตรีมข้อผิดพลาดเป็น null ตามที่แนะนำข้างต้น (ในjava.io.writer(Object)เรียกว่าภายในโดยตัวตรวจสอบ XML) ฉันขอแนะนำให้บันทึกต้นฉบับในฟิลด์แทนoldStdErr = System.errและกู้คืนสิ่งนี้ใน@Afterวิธีการแทน
ลุค Usherwood

6
ทางออกที่ดี เพียงบันทึกสำหรับทุกคนที่ใช้มันคุณอาจต้องตัด () ช่องว่าง / บรรทัดใหม่จาก outContent
อัลลิสัน

102

ฉันรู้ว่านี่เป็นหัวข้อเก่า แต่มีห้องสมุดที่ดีในการทำเช่นนี้:

กฎของระบบ

ตัวอย่างจากเอกสาร:

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
วิธีการนี้เต็มไปด้วยปัญหาเนื่องจากเอาต์พุตสตรีมมาตรฐานเป็นทรัพยากรที่ใช้ร่วมกันที่ใช้โดยส่วนต่างๆของโปรแกรมของคุณ จะเป็นการดีกว่าถ้าใช้ Dependency Injection เพื่อกำจัดการใช้โดยตรงของสตรีมเอาต์พุตมาตรฐาน: stackoverflow.com/a/21216342/545127
Raedwald

30

แทนที่จะเปลี่ยนเส้นทางSystem.outฉันจะ refactor คลาสที่ใช้System.out.println()โดยผ่านการPrintStreamเป็นผู้ทำงานร่วมกันจากนั้นใช้System.outในการผลิตและTest Spyในการทดสอบ นั่นคือใช้ Dependency Injection เพื่อกำจัดการใช้สตรีมเอาต์พุตมาตรฐานโดยตรง

ในการผลิต

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
ฉันไม่สามารถหา ConsoleWriter นี้ได้ทุกที่ใน JDK: อยู่ที่ไหน
Jean-Philippe Caruana

3
มันอาจจะกล่าวถึงในคำตอบ แต่ฉันเชื่อว่าระดับที่ถูกสร้างขึ้นโดยผู้ใช้ 1909402
เซบาสเตียน

6
ฉันคิดว่าConsoleWriterเป็นหัวข้อทดสอบ
Niel de Wet

22

คุณสามารถตั้งค่าสตรีมการพิมพ์ System.out ผ่านsetOut () (และสำหรับinและerr) คุณสามารถเปลี่ยนเส้นทางสิ่งนี้ไปยังกระแสการพิมพ์ที่บันทึกไปยังสตริงแล้วตรวจสอบได้หรือไม่ นั่นจะเป็นกลไกที่ง่ายที่สุด

(ฉันจะสนับสนุนในบางขั้นตอนให้เปลี่ยนแอปเป็นกรอบการบันทึกบางอย่าง - แต่ฉันสงสัยว่าคุณรู้เรื่องนี้อยู่แล้ว!)


1
นั่นคือสิ่งที่อยู่ในใจของฉัน แต่ฉันไม่อยากจะเชื่อเลยว่าไม่มีวิธีมาตรฐานของ JUnit ขอบคุณสมอง แต่เครดิตต้อง dfa สำหรับความพยายามที่เกิดขึ้นจริง
Mike Minicki

วิธีการนี้เต็มไปด้วยปัญหาเนื่องจากเอาต์พุตสตรีมมาตรฐานเป็นทรัพยากรที่ใช้ร่วมกันที่ใช้โดยส่วนต่างๆของโปรแกรมของคุณ จะเป็นการดีกว่าถ้าใช้ Dependency Injection เพื่อกำจัดการใช้โดยตรงของสตรีมเอาต์พุตมาตรฐาน: stackoverflow.com/a/21216342/545127
Raedwald

ใช่. ฉันจะสองที่และบางทีแม้แต่คำถามยืนยันการเข้าสู่ระบบ (ดีกว่าที่จะยืนยันการโทรไปยังองค์ประกอบเข้าสู่ระบบหรือคล้ายกัน)
ไบรอัน Agnew

13

ปิดหัวข้อเล็กน้อย แต่ในกรณีที่บางคน (เช่นฉันเมื่อฉันพบเธรดนี้ครั้งแรก) อาจสนใจบันทึกเอาต์พุตผ่าน SLF4J, 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));
    }
}

ข้อจำกัดความรับผิดชอบ :

  • ฉันพัฒนาห้องสมุดนี้เนื่องจากฉันไม่สามารถหาคำตอบที่เหมาะสมกับความต้องการของฉันเอง
  • ผูกเฉพาะlog4j, log4j2และlogbackที่มีอยู่ในขณะนี้ แต่ผมมีความสุขที่จะเพิ่มมากขึ้น

ขอบคุณมากสำหรับการสร้างห้องสมุดนี้! ฉันกำลังมองหาบางอย่างเช่นนี้มานานแล้ว! มันมีประโยชน์มากเพราะบางครั้งคุณก็ไม่สามารถทำให้โค้ดของคุณง่ายพอที่จะทดสอบได้ง่าย แต่ด้วยข้อความบันทึกคุณสามารถทำสิ่งมหัศจรรย์ได้!
carlspring

สิ่งนี้ดูมีแนวโน้มมาก ... แต่ถึงแม้ว่าฉันเพิ่งคัดลอกโปรแกรม ATMTest ของคุณและเรียกใช้เป็นแบบทดสอบภายใต้ Gradle ฉันได้รับการยกเว้น ... ฉันมีปัญหาในหน้า Github ของคุณ ...
ไมค์หนู

9

@dfa คำตอบดีมากดังนั้นฉันจึงก้าวไปอีกขั้นเพื่อให้สามารถทดสอบกลุ่มของ ouput ได้

ครั้งแรกที่ฉันสร้างขึ้นTestHelperด้วยวิธีการที่ยอมรับระดับcaptureOutput annoymous CaptureTestเมธอด captureOutput ทำงานของการตั้งค่าและการแยกสตรีมเอาต์พุต เมื่อการดำเนินการCaptureOutputของtestวิธีการที่เรียกว่ามันมีการเข้าถึงการส่งออกสร้างบล็อกสำหรับการทดสอบ

แหล่งที่มาสำหรับ 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 ถูกกำหนดไว้ในไฟล์เดียวกัน

จากนั้นในการทดสอบของคุณคุณสามารถนำเข้าจับภาพคงที่ออก นี่คือตัวอย่างการใช้ 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 = ใหม่ 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เพราะผมไม่ได้ต้องการมัน System.outแต่มันควรจะเป็นเรื่องง่ายที่จะดำเนินการคล้ายกับการทดสอบ


1

คุณไม่ต้องการเปลี่ยนเส้นทางสตรีม system.out เนื่องจากการเปลี่ยนเส้นทางสำหรับ ENTIRE JVM อะไรก็ตามที่ทำงานบน JVM สามารถทำให้เกิดความสับสนได้ มีวิธีที่ดีกว่าในการทดสอบอินพุต / เอาต์พุต มองเข้าไปในต้นขั้ว / mocks


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

คุณไม่สามารถพิมพ์โดยตรงโดยใช้System.out.printlnหรือใช้API ตัดไม้ในขณะที่ใช้JUnit แต่ถ้าคุณต้องการตรวจสอบค่าใด ๆ คุณก็สามารถใช้งานได้

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

สำหรับการตั้งค่าและตรรกะ teardown แบบนี้ฉันจะใช้@Ruleแทนที่จะทำแบบอินไลน์ในการทดสอบของคุณ โดยเฉพาะอย่างยิ่งหากการยืนยันของคุณล้มเหลวSystem.setOut/Errจะไม่สามารถติดต่อสายที่สองได้
dimo414
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.