I have a singleton class to help me reading input from the console:
public class IOHelper {
public org.slf4j.Logger logger = Logger.logger;
//JLine
public ConsoleReader cr;
private static IOHelper instance;
private IOHelper(){
{
try {
cr = new ConsoleReader();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static synchronized IOHelper getInstance(){
if (instance == null){
instance = new IOHelper();
}
return instance;
}
The code which I'd like to test refers to it as:
String in = IOHelper.getInstance().cr.readLine();
Then my test class:
class Test {
private static NetworkCommunicator networkCommunicator;
private static IOHelper ioHelper;
@BeforeAll
static void setUpClass() throws Throwable {
ioHelper = spy(IOHelper.getInstance());
doReturn("1").when(ioHelper).cr.readLine();
networkCommunicator = spy(NetworkCommunicator.class);
doNothing().when(networkCommunicator).connectToServer();
doNothing().when(networkCommunicator).connectToOtherServer();
}
My test gets stuck on the doReturn("1").when(ioHelper).cr.readLine();
line as if it actually executed the cr.readline();
part. My stacktrace points towards the method private native int read0() throws IOException;
found on FileInputStream. The comments suggest it blocks if no input is available.
I want to replace the method readLine()
on my console, so when my CLI asks for an input, my test can "fake" that input.
edit: the call stack of the 2 interesting threads:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
blocks NonBlockingInputStreamThread@1437
at java.io.FileInputStream.read0(FileInputStream.java:-1)
at java.io.FileInputStream.read(FileInputStream.java:207)
at jline.internal.NonBlockingInputStream.read(NonBlockingInputStream.java:166)
- locked <0x67d> (a jline.internal.NonBlockingInputStream)
at jline.internal.NonBlockingInputStream.read(NonBlockingInputStream.java:135)
at jline.internal.NonBlockingInputStream.read(NonBlockingInputStream.java:243)
at jline.internal.InputStreamReader.read(InputStreamReader.java:257)
at jline.internal.InputStreamReader.read(InputStreamReader.java:194)
at jline.console.ConsoleReader.readCharacter(ConsoleReader.java:2147)
at jline.console.ConsoleReader.readCharacter(ConsoleReader.java:2137)
at jline.console.ConsoleReader.readBinding(ConsoleReader.java:2222)
at jline.console.ConsoleReader.readLine(ConsoleReader.java:2463)
at jline.console.ConsoleReader.readLine(ConsoleReader.java:2374)
at jline.console.ConsoleReader.readLine(ConsoleReader.java:2362)
at jline.console.ConsoleReader.readLine(ConsoleReader.java:2350)
at com.mypkg.Test.setUpClass(Test.java:43)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:389)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$invokeBeforeAllMethods$5(ClassTestDescriptor.java:228)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor$$Lambda$162.715378067.execute(Unknown Source:-1)
at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.invokeBeforeAllMethods(ClassTestDescriptor.java:227)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.before(ClassTestDescriptor.java:151)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.before(ClassTestDescriptor.java:61)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$execute$3(HierarchicalTestExecutor.java:80)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$$Lambda$134.398690014.execute(Unknown Source:-1)
at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:77)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$null$2(HierarchicalTestExecutor.java:92)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$$Lambda$137.1353170030.accept(Unknown Source:-1)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
at java.util.Iterator.forEachRemaining(Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$execute$3(HierarchicalTestExecutor.java:92)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$$Lambda$134.398690014.execute(Unknown Source:-1)
at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:77)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:51)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:62)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
"NonBlockingInputStreamThread@1437" daemon prio=5 tid=0xf nid=NA waiting
java.lang.Thread.State: WAITING
waiting for main@1 to release lock on <0x67d> (a jline.internal.NonBlockingInputStream)
at java.lang.Object.wait(Object.java:-1)
at jline.internal.NonBlockingInputStream.run(NonBlockingInputStream.java:275)
at java.lang.Thread.run(Thread.java:745)
Extending this question: I have some methods which asks the user multiple inputs (eg. updating some settings). Am I correct to think that the best approach would be to refactor the settings to a method which takes arguments and test only this new method? Is there a solution where I could just pass down a series of strings to the test to press when any method is trying to read from the ConsoleReader
? I thought of using Robot
but how can I make sure that it passes down the keystrokes in a correct order if the reading is not done by the test rather than the underlying logic?
Seems like you're mocking the wrong thing. You're wanting to play with the ConsoleReader
's returns. So a couple of options:
Move to use some getConsoleReader()
method on your IOHelper
class, which you can then mock out - you'll need to make sure that the IOHelper
class also accesses this via this method. E.g.
private final IOHelper mySpy = spy(IOHelper.getInstance());
@Before
public void setup() {
final ConsoleReader mockCR = mock(ConsoleReader.class);
// Any mockery on your mockCR you need.
// doReturn(...).when(mockCR).readLine();, etc.
doReturn(mockCR).when(mySpy).getConsoleReader();
}
Modify the cr field to be a mock. E.g.
private final IOHelper ioHelper= IOHelper.getInstance();
@Before
public void setup() {
final ConsoleReader mockCR = mock(ConsoleReader.class);
// Any mockery on your mockCR you need.
// doReturn(...).when(mockCR).readLine();, etc.
ioHelper.cr = mockCR;
}
I'd warn against this case though; I can't see a reason to have your ConsoleReader
be public
(or not be final
), and this just makes that a requirement. You can always use some helper library to mess with the field even if it's private. spring and apache-commons-lang3 both provide this type of utility.
Use Powermock to fiddle with the constructor of ConsoleReader
:
@RunWith(PowerMockRunner.class)
@PrepareForTest(IOHelper.class)
public class IOHelperTest {
@BeforeClass
public static void setup() {
final ConsoleReader mockCR = mock(ConsoleReader.class);
// Any mockery on your mockCR you need.
// doReturn(...).when(mockCR).readLine();, etc.
PowerMock.whenNew(ConsoleReader.class).thenReturn(mockCR);
}
}
Finally, you could modify your IOHelper
class to take a ConsoleReader
as a constructor argument, and simply provide the mockCR
from all the above approaches to that (and make it not private
).