0

I cannot refactor the code, I must test how it is written. I cannot figure out how to test the if (reader.hasNextLong()) line of code:

public class UnitsConvertor {
// No instances please. All methods are static.
private UnitsConvertor() {
}

public static void main(String[] args) {
    // parses user input, checking both integer and string
    System.out.println("Please Enter the input value followed by the unit:");
    Scanner reader = new Scanner(System.in);
    if (reader.hasNextLong()) {
        long number = reader.nextLong();
        if (reader.hasNext("\\b+(mil|in|inch|ft|foot|feet|yd|yard|mi|mile)\\b+")) {
            String unit = reader.findInLine("\\b+(mil|in|inch|ft|foot|feet|yd|yard|mi|mile)\\b+");
            double mm = toMm(number, unit);
            System.out.println(number + " " + unit + " is:");
            System.out.println(String.format("%f", mm) + " mm");
            System.out.println(String.format("%f", mm / 10) + " cm");
            System.out.println(String.format("%f", mm / 1000) + " m");
            System.out.println(String.format("%f", mm / 1000000) + " km");
        } else if (reader.hasNext("\\b+(mm|millimeter|cm|centimeter|m|meter|km|kilometer)\\b+")) {
            String unit = reader.findInLine("\\b+(mm|millimeter|cm|centimeter|m|meter|km|kilometer)\\b+");
            double mil = toMil(number, unit);
            System.out.println(number + " " + unit + " is:");
            System.out.println(String.format("%.2g", mil) + " mil");
            System.out.println(String.format("%.2g", mil / 1000) + " inch");
            System.out.println(String.format("%.2g", mil / 12000) + " ft");
            System.out.println(String.format("%.2g", mil / 36000) + " yard");
            System.out.println(String.format("%.2g", mil / 63360000) + " mile");
        } else {
            System.out.println("Invalid input");
        }
    } else {
        System.out.println("Invalid input");

    }
    reader.close();
    return;
}

// convert any metric system with unit specified in second parameter to mil
public static double toMil(long metric, String unit) {
    double mm;
    if (unit.matches("\\b+(mm|millimeter)\\b+")) {
        mm = metric;
    } else if (unit.matches("\\b+(cm|centimeter)\\b+")) {
        mm = metric * 10;
    } else if (unit.matches("\\b+(m|meter)\\b+")) {
        mm = metric * 1000;
    } else if (unit.matches("\\b+(km|kilometer)\\b+")) {
        mm = metric * 1000000;
    } else {
        throw new IllegalArgumentException("Bad unit value");
    }
    return mm * 39.3701;
}

// convert any imperial system with unit specified in second parameter to mm
public static double toMm(long imperial, String unit) {
    double mil;
    if (unit.matches("\\b+(in|inch)\\b+")) {
        mil = imperial * 1000;
    } else if (unit.matches("\\b+(ft|foot|feet)\\b+")) {
        mil = imperial * 12000;
    } else if (unit.matches("\\b+(yd|yard)\\b+")) {
        mil = imperial * 36000;
    } else if (unit.matches("\\b+(mi|mile)\\b+")) {
        mil = imperial * 63360000;
    } else if (unit.matches("\\b+mil\\b+")) {
        mil = imperial;
    } else {
        throw new IllegalArgumentException("Illegal unit value.");
    }
    return mil * 0.0254;
}

}

Without the testHasNextLong() I reach 98.2% of code coverage. The only yellow and red highlights from code coverage show that hasNextLong(), the else if (reader.hasNext("\b+(mm|millimeter|cm|centimeter|m|meter|km|kilometer)\b+")), and the 2 "elses" that contain System.out.println("Invalid input"); are not covered. When I add in the hasNextLong test only 10/23 tests are run. Without it 22/22 are run.

Here are all the tests I have written:

class UnitsConvertorTest {

private final InputStream systemIn = System.in;
private final PrintStream systemOut = System.out;
private ByteArrayInputStream testIn;
private ByteArrayOutputStream testOut;
private String userUnitInput;
private Long userValueInput;

/**
 * @throws java.lang.Exception
 */
@BeforeEach
void setUp() throws Exception {
    testOut = new ByteArrayOutputStream();
    System.setOut(new PrintStream(testOut, true));
}

/**
 * @throws java.lang.Exception
 */
@AfterEach
void tearDown() throws Exception {
     System.setIn(systemIn);
     System.setOut(systemOut);
}

@Test
public void testHasNextLong() {
  final String testString = "10 mil";
      System.setIn(new ByteArrayInputStream(testString.getBytes()));
      Scanner scanner = new Scanner(systemIn);
      System.out.println("" + scanner.hasNextLong());
      assertTrue(scanner.hasNextLong());
      scanner.close();
      System.setIn(systemIn);
  }


// we have a long and a string
// test when string = mm
@Test
void mmTest() {
    userUnitInput = "mm";
    userValueInput = (long) 10;

    assertEquals(393.701, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = millimeter
@Test
void millimeterTest() {
    userUnitInput = "millimeter";
    userValueInput = (long) 10;
    assertEquals(393.701, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = cm
@Test
void cmTest() {
    userUnitInput = "cm";
    userValueInput = (long) 10;
    assertEquals(3937.01, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = centimeter
@Test
void centimeterTest() {
    userUnitInput = "centimeter";
    userValueInput = (long) 10;
    assertEquals(3937.01, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = m
@Test
void mTest() {
    userUnitInput = "m";
    userValueInput = (long) 10;
    assertEquals(393701, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = meter
@Test
void meterTest() {
    userUnitInput = "meter";
    userValueInput = (long) 10;
    assertEquals(393701, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = km
@Test
void kmTest() {
    userUnitInput = "km";
    userValueInput = (long) 10;
    assertEquals(393701000, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = kilometer
@Test
void kilometerTest() {
    userUnitInput = "kilometer";
    userValueInput = (long) 10;
    assertEquals(393701000, UnitsConvertor.toMil(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = in
@Test
void inTest() {
    userUnitInput = "in";
    userValueInput = (long) 10;

    assertEquals(254, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = inch
@Test
void inchTest() {
    userUnitInput = "inch";
    userValueInput = (long) 10;
    assertEquals(254, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = ft
@Test
void ftTest() {
    userUnitInput = "ft";
    userValueInput = (long) 10;
    assertEquals(3048, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = foot
@Test
void footTest() {
    userUnitInput = "foot";
    userValueInput = (long) 10;
    assertEquals(3048, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = feet
@Test
void feetTest() {
    userUnitInput = "feet";
    userValueInput = (long) 10;
    assertEquals(3048, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = yd
@Test
void ydTest() {
    userUnitInput = "yd";
    userValueInput = (long) 10;
    assertEquals(9144, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = yard
@Test
void yardTest() {
    userUnitInput = "yard";
    userValueInput = (long) 10;
    assertEquals(9144, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = mi
@Test
void miTest() {
    userUnitInput = "mi";
    userValueInput = (long) 10;
    assertEquals(16093440, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = mile
@Test
void mileTest() {
    userUnitInput = "mile";
    userValueInput = (long) 10;
    assertEquals(16093440, UnitsConvertor.toMm(userValueInput, userUnitInput));

}

// we have a long and a string
// test when string = mil
@Test
void milTest() {
    userUnitInput = "mil";
    userValueInput = (long) 10;
    assertEquals(.254, UnitsConvertor.toMm(userValueInput, userUnitInput));
}

// Testing IllegalArgumentException when a user enters a decimal value
@Test
void testExpectedExceptionToMil() {
    Assertions.assertThrows(IllegalArgumentException.class, () -> {
        UnitsConvertor.toMil(10, "mizx");
    });
}

@Test
void testExpectedExceptionToMm() {
    Assertions.assertThrows(IllegalArgumentException.class, () -> {
        UnitsConvertor.toMm(10, "mike");
    });
}

// normalizeExpectedOutput - generate the eol character at run-time. // then
// there is no need to hard-code "\r\n" or "\n" for eol
// and string comparisons are portable between Windows, macOS, Linux.
public String normalizeExpectedOutput(String expectedOutput) {
    String normExpectedOutput;
    String[] outputs = expectedOutput.split("\n");
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);

    for (String str : outputs) {

        pw.println(str);
    }

    pw.close();
    normExpectedOutput = sw.toString();
    return normExpectedOutput;
}

@Test
// test that user input returns calculation conversions correctly
public void testMainToMm() {
    String inputValueAndUnit = "10 mil";
    Long inputValue = 10L;
    // save current System.in and System.out
    InputStream myIn = new ByteArrayInputStream(inputValueAndUnit.getBytes());
    System.setIn(myIn);

    double mm = UnitsConvertor.toMm(inputValue, "mil");
    final String unNormalizedExpectedOutput = "Please Enter the input value followed by the unit:\n"
            + inputValueAndUnit + " is:\n" + String.format("%f", mm) + " mm\n" + String.format("%f", mm / 10)
            + " cm\n" + String.format("%f", mm / 1000) + " m\n" + String.format("%f", mm / 1000000) + " km";

    final String expectedOutput = normalizeExpectedOutput(unNormalizedExpectedOutput);

    UnitsConvertor.main(null);

    // Check results
    final String printResult = testOut.toString();
    assertEquals(expectedOutput, printResult);

}

@Test
// get invalid number
public void testMainToMil() {
    String inputValueAndUnit = "10 mm";
    Long inputValue = 10L;
    // save current System.in and System.out
    InputStream myIn = new ByteArrayInputStream(inputValueAndUnit.getBytes());
    System.setIn(myIn);

    double mil = UnitsConvertor.toMil(inputValue, "mm");
    final String unNormalizedExpectedOutput = "Please Enter the input value followed by the unit:\n"
            + inputValueAndUnit + " is:\n" + String.format("%.2g", mil) + " mil\n"
            + String.format("%.2g", mil / 1000) + " inch\n" + String.format("%.2g", mil / 12000) + " ft\n"
            + String.format("%.2g", mil / 36000) + " yard\n" + String.format("%.2g", mil / 63360000) + " mile";

    final String expectedOutput = normalizeExpectedOutput(unNormalizedExpectedOutput);

    UnitsConvertor.main(null);

    // Check results
    final String printResult = testOut.toString();
    assertEquals(expectedOutput, printResult);

}

}

taraloca
  • 9,077
  • 9
  • 44
  • 77
  • What is `systemIn`? Do you want to test the `main()` method? What is `provideInput`? How do the other tests fail when you add your test method? What are the other test methods doing? – Progman Feb 14 '21 at 17:45
  • I have edited my question and now show all the code. I removed the provideInput. I need to test the main(). – taraloca Feb 15 '21 at 13:22

2 Answers2

1

You can't unit test this code.

Refactor the main into methods; leave the input and output of it. You don't need to test input and output. It's your unit conversions that you need to make sure are correct.

You're writing a unit conversion method. Make it a separate method.

public double convertLength(double fromValue, String fromUnit, String toUnit) {
    // put your conversion code here.  No I/O; just do the conversion
}

Maybe you need a class to encapsulate the value with its units.

duffymo
  • 305,152
  • 44
  • 369
  • 561
  • My assignment is to write unit tests for the code provided and to have at least 95% of code coverage. I am not allowed to refactor, though I already had thought exactly what your response is....refactor. So now what? I have updated my post to show the full code source. – taraloca Feb 15 '21 at 13:15
0

When you want to test the main() method for the hasNextLong() call in your testHasNextLong() test method, you actually have to call the main() methods like you did in the test methods testMainToMil() and testMainToMm().

But more importantly, you are closing the (default) System.in stream with the line

Scanner scanner = new Scanner(systemIn);
// ...
scanner.close();

making it unusable for any further test calls (assuming the same thread or java process is using to run all tests). This issue is described in other questions like What does scanner.close() do? and Close a Scanner linked to System.in.

Instead of testing a separate instance of Scanner and calling hasNextLong() on it (which has nothing to do with the code coverage of your main() method), you have to create a test method like testMainToMil() or testMainToMm() by providing an input string which steers you through the correct if/else statements you want to cover with your coverage test. Depending on where the test execution thread should go, you can test the main() method with inputs like "10 invalid" or "dummy", which should reach the else statements with the System.out.println("Invalid input"); lines.

Progman
  • 16,827
  • 6
  • 33
  • 48
  • I appreciate your analyzation, but I do not get how the heck to test the else without testing the scanner object. I am very new to this and I honestly don't understand why some of my tests even work. For instance, why does my code coverage show that I covered the second if statement but not the first and third (else/if)? – taraloca Feb 16 '21 at 00:41
  • @taraloca You don't want to test the `Scanner` object from your test, you want to test the `Scanner` object from *inside* the `main()` method. Actually, you want to test the several `if`/`else` branches you have. With `testMainToMil` and `testMainToMm` you already check two paths, but there are more. So write additional test methods which check different paths. As written in the answer, you might want to test execution paths where the keyboard input is `"dummy"` (where the overall input is invalid) or `"10 invalid"` (where the number is correct, but the unit is incorrect). – Progman Feb 16 '21 at 16:10