How to mock System.in? [duplicate]
Asked Answered
D

1

6

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

Dextro answered 20/9, 2018 at 20:32 Comment(1)
I'm afraid someService's code is kind of crucial here. It works alright for me.Whorton
W
6

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.

Whorton answered 20/9, 2018 at 20:43 Comment(18)
should I inject InputStream in my test class? I can't change the src code, I have to write integration tests for itDextro
Ah ok, annoying. I updated the answer accordingly.Whorton
Thank you. If I run them 1 by 1 they pass but if I run them on a class level they fail even with your @AfterDextro
I think one of your tests isn't setting stdin properly, that was already the reason for the first errorWhorton
But I copied your piece of codeDextro
My piece of code doesn't guarantee the test works. You also have to set stdin to a custom buffer before every test, and I'm not sure you're doing that. The code on my laptop is working fine even with several tests. Could you share more of your code?Whorton
I updated it. Could you look at it, please?Dextro
Hmm, they seem correctly set up to meWhorton
In your run method, is stdin a static variable?Whorton
Also, you don't reset your output buffer...Whorton
Works fine for me: pastebin.com/JFGQuXC9Whorton
yes, private static Scanner stdin;Dextro
sorry, seems to be my bad, I'm in 10th position meanwhile the size of buf is 10. But still strange that the tests are passed 1 by 1Dextro
Yeah I agree, sounds weirdWhorton
Hi Dici, thank you for assistance. Could I share my project with you and you try to run it?Dextro
Hey, I don't know if I'm that motivated. I thought I understood that you found your problem. Are you still having issues?Whorton
Yes, I have them in case I run all the test (click button run on class level)Dextro
You said that there was a bug in your code though, I thought this was the reason for your problem. Could you share your code on gist (as small as possible)?Whorton

© 2022 - 2024 — McMap. All rights reserved.