1

I want to test method setDay() using arguments [-1,0,24,2,32].

I know that Scanner reader can be

String test="-1 0 24 2 32"; Scanner reader=new Scanner(test);

The main problem is infinite loop and void method. How can we test this kind of code? Here is example code:

public NameOfTheDay(){
   int day=1;     
}
{...}
public void setDay(Scanner reader) {
    while (true) {
        System.out.print("Day: ");
        String input = reader.nextLine();
        if (input.matches("\\d{2}")) {
            int day = Integer.parseInt(input);
            if (day > 0 && day < 32) {
                this.day = day;
                return;
            }
        }
        System.out.println("Wrong day. Try again.");
    }
}

Thanks for the answer.

Kapsel
  • 13
  • 1
  • 5
  • 1
    Your code simply is wrong, for the input "ab" the method will never return. – luk2302 Mar 01 '18 at 11:23
  • `String input = reader.nextLine();` should be in the while loop – Oneiros Mar 01 '18 at 11:27
  • And that's my intention to return when input will be number. But it isn't my question. I want to know how can I check this method. I search similar topics but never gets proper answer. @Oneiros right. Edited – Kapsel Mar 01 '18 at 11:28
  • It looks like a very weird unit test to me. Why should you test the functioning of the java scanner? Unit tests should not need user inputs, don't you execute this code procedurally along other tests? I would test just the condition on the day integer... am I getting it wrong? – Oneiros Mar 01 '18 at 11:33
  • 1
    The only way you can test this is by mocking the system console pretty much (because that's the only way your method communicates failures). I strongly advise you to do it any other way that won't require this. – M. Prokhorov Mar 01 '18 at 11:41
  • This method do to much, a unit test should be restricted for "unit functionnality". The method `setDay` should accept a number, not a `Scanner`. And then you have to read the `Scanner` before you call that method. The only part you have to test is the "filter" of the input, this is a method by itself `public boolean isValid(String input, String pattern)` and this is easy to unit test. – AxelH Mar 01 '18 at 11:41
  • I don't know if this code is worth testing. I'm beginner. In extended version i made more "if statements" (days depends on month). I'm sure that is the way to test this code. The main problem for me is that is void method plus infinite loops (when all numbers will be out of range). – Kapsel Mar 01 '18 at 11:44
  • @Kapsel, technically everythin is worth testing. But this method does pose a problem because it's written specifically in a way that's very hard to test. Change the method and code around it, then it will be OK. – M. Prokhorov Mar 01 '18 at 11:47
  • Thanks for answers. I'll try divide this code into smaller pieces. – Kapsel Mar 01 '18 at 11:49
  • @Kapsel please consider accepting an answer (maybe wait some days for better answers) or answer the question by yourself, by this you help others reading this question. – Florian Albrecht Mar 01 '18 at 11:54
  • @Florian Albrecht When I check it and I'll accept it. I'm new in SO, so i learn principles of this site – Kapsel Mar 01 '18 at 11:59
  • In essence:your code is hard to test (you are mixing various responsibilities --- like *computing* some information and *printing* it). So you start by **not** doing several things in one method. And then you follow the advise given in the answers/comments.In other words: the current method could be tested by checking what gets written to stdout (therefore the duplicated question). But the real answer is to change your code to make it easier to test. – GhostCat Mar 02 '18 at 08:06

2 Answers2

2

How can we test this kind of code?

You cannot.

unittest verify the public observable behavior of your code under test where "public observable behavior" is any rreturn value or communication with dependencies.

Communication with dependencies is checked with test doubles which we (usually) create using a mocking framework and which we inject into the code under test.

A major prerequest is that you code cleanly incorporates Single Responsibility/Separation of Concerns pattern.

Your code does not return anything and has no possibility to replace the dependencies of interest (here System.out) because it mixes business logic with user interaction.


Some may argue, that you can assign a test double to System.out or use PowerMock to replace dependencies but IMHO this is just a surrender to your bad design and will not pay off as your program grows.

Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51
  • Unit tests is not the only kind of tests. There are also integration test, end-to-end tests in live systems, etc. It's incorrect to say "you cannot test this". You can, but not with unit tests. – M. Prokhorov Mar 01 '18 at 11:48
  • I disagree. The method changes `this.day`, so there **is** observable behaviour other than System.out output. – Florian Albrecht Mar 01 '18 at 11:48
  • There also exists a way to set `System.out`, simply by calling `System.setOut()`. – M. Prokhorov Mar 01 '18 at 11:50
  • @FlorianAlbrecht *"The method changes `this.day`, so there is observable behaviour other than System.out output"* `this.day` is part of the *internal state* of the cut and should not be used in UnitTesting. – Timothy Truckle Mar 01 '18 at 15:30
  • @M.Prokhorov direct use of a *global variable* (which `System.out` surely is) is not a good idea, especially when testing... – Timothy Truckle Mar 01 '18 at 15:32
  • @TimothyTruckle, yes, it's generally a bad idea. But if it's necessary, and one cleans up after themselves, then it can have its place (preferrably as little as possible, ofc). – M. Prokhorov Mar 01 '18 at 16:36
1

I will not focus on the contents of your method, but just on the question on how to unit test a method expecting a Scanner object as parameter.

The easy answer is: Provide your test input data as a String, and build a scanner around it, like this:

@Test
public void testSetDay_positive() {
    String testInput = "23\n";
    Scanner testScanner = new Scanner(testInput);
    NameOfTheDay notd = new NameOfTheDay();
    notd.setDay(testScanner);
    Assert.assertEquals(23, notd.getDay()); // or whatever condition to test
}

Now it gets harder. Perhaps you want to test an invalid input first, and make sure the second input is used then?

@Test
public void testSetDay_negative_then_positive() {
    String testInput = "999\n23\n"; // two lines of input here
    Scanner testScanner = new Scanner(testInput);
    NameOfTheDay notd = new NameOfTheDay();
    notd.setDay(testScanner);
    Assert.assertEquals(23, notd.getDay()); // or whatever condition to test
}

If you want to test if the error message is written to System.out, you would have to replace that with a custom stream to test afterwards:

ByteArrayOutputStream mockOut = new ByteArrayOutputStream();
PrintStream newOut = new PrintStream(mockOut);
System.setOut(newOut);
// execute test from above
Assert.assertTrue(new String(mockOut.toByteArray(), "UTF-8").contains("Wrong day. Try again."));

Still, most comments to your question contain valuable input (move validation to an extra method etc.) which should be considered.

Florian Albrecht
  • 2,266
  • 1
  • 20
  • 25