Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock System.in? [duplicate]

Could you explain me please how to mock System.in correctly?

I have a simple test and I assign values to

@Test
public void testBarCorrect() {
    System.setIn(new ByteArrayInputStream("7\n2\n3".getBytes()));
    someService().consume();
}

Under the hood I do nothing but new Scanner(System.in);

The caveat is that sometimes my test is green but in 80% it throws an Exception "java.util.NoSuchElementException: No line found".

So, if I press run on a test it misbehaves.

Could you give me any hints?

My test:

public class RecordServiceCTLIntegrationTest {

    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
    private final PrintStream originalOut = System.out;
    private static final InputStream DEFAULT_STDIN = System.in;

    @Before
    public void setUpStreams() {
        System.setOut(new PrintStream(outContent));
        System.setIn(DEFAULT_STDIN);
    }

    @After
    public void rollbackChangesToStdin() {
        System.setOut(originalOut);
        System.setIn(DEFAULT_STDIN);
    }

    @Test
    public void testBarCorrect() {
        System.setIn(new ByteArrayInputStream("5\n1\nB\n21\n\n".getBytes()));
        buildRecordServiceCTL().run();
        assertEquals("Enter Room Id: No active booking for room id: 5\n" +
                "Enter Room Id: B:\tBar Fridge\n" +
                "R:\tRestaurant\n" +
                "S:\tRoom Service\n" +
                "Enter service typeEnter cost: Service Bar Fridge has been added for the roomNumber 1\n" +
                "Hit <enter> to continuePay for service completed\n", outContent.toString());
    }

    @Test
    public void testRestaurantCorrect() {
        System.setIn(new ByteArrayInputStream("5\n1\nR\n21\n\n".getBytes()));
        buildRecordServiceCTL().run();
        assertEquals("Enter Room Id: No active booking for room id: 5\n" +
                "Enter Room Id: B:\tBar Fridge\n" +
                "R:\tRestaurant\n" +
                "S:\tRoom Service\n" +
                "Enter service typeEnter cost: Service Restaurant has been added for the roomNumber 1\n" +
                "Hit <enter> to continuePay for service completed\n", outContent.toString());

    }

    @Test
    public void testRoomServiceCorrect() {

        System.setIn(new ByteArrayInputStream("5\n1\nS\n21\n\n".getBytes()));
        buildRecordServiceCTL().run();
        assertEquals("Enter Room Id: No active booking for room id: 5\n" +
                "Enter Room Id: B:\tBar Fridge\n" +
                "R:\tRestaurant\n" +
                "S:\tRoom Service\n" +
                "Enter service typeEnter cost: Service Room Service has been added for the roomNumber 1\n" +
                "Hit <enter> to continuePay for service completed\n", outContent.toString());
    }

    @Test(expected = NoSuchElementException.class)
    public void testRoomException() {
        System.setIn(new ByteArrayInputStream("-1\n\n".getBytes()));
        buildRecordServiceCTL().run();
    }

    @Test
    public void cancel() {
        System.setIn(new ByteArrayInputStream("5\n1\nS\n20\n".getBytes()));
        assertEquals("", outContent.toString());
    }

    private RecordServiceCTL buildRecordServiceCTL() {
        Hotel hotel = new Hotel();
        final Guest guest = new Guest("name", "address", 123);
        final Room room = new Room(1, SINGLE);
        final CreditCard card = new CreditCard(VISA, 123, 123);
        final Booking booking = new Booking(guest, room, new Date(), 10, 1, card);
        hotel.activeBookingsByRoomId.put(room.getId(), booking);
        final Map<Integer,Room> map = new HashMap<>();
        map.put(room.getId(), room);
        hotel.roomsByType.put(SINGLE, map);
        return new RecordServiceCTL(hotel);
    }

}

And exception occurs in run method in:

if (stdin == null) {
    stdin = new Scanner(System.in);
}
String ans = stdin.nextLine();

enter image description here

like image 411
Pasha Avatar asked Sep 20 '18 20:09

Pasha


1 Answers

This test works fine for me:

@Test
public void testBarCorrect() {
    System.setIn(new ByteArrayInputStream("7\n2\n3".getBytes()));
    Scanner scanner = new Scanner(System.in);
    System.out.println(scanner.nextLine());
    System.out.println(scanner.nextLine());
    System.out.println(scanner.nextLine());
}

However, if I add the following test, this fails:

@Test
public void testFooCorrect() {
    Scanner scanner = new Scanner(System.in);
    System.out.println(scanner.nextLine());
    System.out.println(scanner.nextLine());
    System.out.println(scanner.nextLine());
}

java.util.NoSuchElementException: No line found
    at java.util.Scanner.nextLine(Scanner.java:1540)
    at com.amazon.adcs.service.dra.domain.A9DeviceTest.testFooCorrect(A9DeviceTest.java:15)

In theory a unit test should leave the JVM in the same state it was before the execution of the test. It's not your case since you have mutated a singleton state, and you don't rollback the modification after the test. If you have multiple tests using System.in, the first test might be consuming all of it and leaving nothing to the other one.

Normally, I would recommend you to inject an InputStream in your class and then you're free to do whatever you want without affecting a system constant. Since you tell me that you cannot edit the code, then you should make sure you clean up the state after yourself.

Something like this will do the trick:

private static final InputStream DEFAULT_STDIN = System.in;

@After
public void rollbackChangesToStdin() {
    System.setIn(DEFAULT_STDIN);
} 

If you have to do this often, you might want to consider implementing a JUnit rule.

like image 60
Dici Avatar answered Oct 09 '22 23:10

Dici