可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have been asked to introduce unit test in a legacy Java Application that runs and operates from Command Line. Basically the main loop prints out a Menu, the user inputs something and it shows more data.
This Main class illustrate how the application works.
public class Main{
static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
public static void main(String argv[]) throws IOException{
while (true) {
char input = (char) reader.read();
if(input == 'x'){
return;
}
System.out.println(input);
}
}
}
I'd like my test methods to look something like this
public void testCaseOne(){
Main.main();
String result = "";
result = sendInput("1");
assertEqual(result, "1");
result = sendInput("x");
assertEqual(result,"");
}
I am aware of the System.setOut()
and System.setIn()
methods, but I cannot figure out a way to make the System.setIn()
method work in this context, since the reader.read()
method is blocking my thread.
Is my test design wrong?
Is there a way to design the sendInput()
method to work through the blocking reader.read() call?
回答1:
I would suggest refactoring the code to allow the input/output streams to be injected, and then you can mock them. If you couuld change it to something like
public class Main{
static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
public static void main(String argv[]) throws IOException{
new YourClass(reader,System.out).run();
}
}
public class YourClass { // I don't know what your class is actually doing, but name it something appropriate
private final InputReader reader;
private final PrintStream output;
public YourClass(InputReader reader, PrintStream output) {
this.reader = reader;
this.output = ouptut;
}
public void run() {
while (true) {
char input = (char) reader.read();
if(input == 'x')
return;
output.println(input);
}
}
This design does a couple of things:
It takes the logic out of your main class. Typically a main method is really just used for launching an application.
It makes YourClass
more easily unit testable. In your tests, you can simply mock out the input/output.
Edit: Update to how this refactoring helps with the blocking IO problem
By making the reader/output injectable as shows above, you don't actually need to use the real System.in and System.out - you can use a mock instead. This eliminates the need to actually have blocking reads.
public void testCaseOne(){
// pseudocode for the mock - this will vary depending on your mock framework
InputReader reader = createMock(InputReader);
// the first time you read it will be a "1", the next time it will be an "x"
expect(reader.read()).andReturn("1");
expect(reader.read()).andReturn("x");
PrintStream stream = createMock(PrintStream);
// only expect the "1" to get written. the "x" is the exit signal
expect(stream.println("1"));
new YourClass(reader,stream).run();
verifyMocks();
}
回答2:
I would refactor Main so it's easier to test.. like so:
public class Main{
private boolean quit = false;
public static void main(String[] argv) throws IOException {
Main main = new Main();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
char input = main.readInput(reader);
while (!main.quit()) {
System.out.println(input);
input = main.readInput(reader);
}
}
public char readInput(Reader reader) throws IOException{
char input = (char) reader.read();
if(input == 'x'){
quit = true;
return '\0';
}
return input;
}
public boolean quit(){
return quit;
}
}
Personally, I try to stay away from static variables. If you need one you could always declare it in the main method like above.
Testing the while(true) is pretty much impossible because testing if the while loop never quits would take an infinite amount of time. Then there is the question if you should test the quitting of the loop in the main.quit() == true
case. Personally, I would just test the core logic and leave the rest untested:
public class MainTest {
private Main main;
@Before
public void setup(){
main = new Main();
}
@Test
public void testCaseOne() throws IOException{
char result1 = main.readInput(new StringReader("1"));
assertEquals(result1, '1');
assertFalse(main.quit());
char result2 = main.readInput(new StringReader("x"));
assertEquals(result2, '\0');
assertTrue(main.quit());
}
}
回答3:
Here is the solution I went with that required no refactoring of the legacy code.
In a nutshell, I made an Abstract Test Class that compiles and execute the Application in a Process on a seperate thread. I attach myself to the Input/Output of the Process and read/write to it.
public abstract class AbstractTest extends TestCase{
private Process process;
private BufferedReader input;
private BufferedWriter output;
public AbstractTest() {
//Makes a text file with all of my .java files for the Java Compiler process
Process pDir = new ProcessBuilder("cmd.exe", "/C", "dir /s /B *.java > sources.txt").start();
pDir.waitFor();
//Compiles the application
Process p = new ProcessBuilder("cmd.exe", "/C", "javac @sources.txt").start();
p.waitFor();
}
protected void start(){
Thread thread = new Thread() {
public void run() {
//Execute the application
String command = "java -cp src/main packagename.Main ";
AbstractTest.this.process = = new ProcessBuilder("cmd.exe", "/C", command).start();
AbstractTest.this.input = new BufferedReader(new InputStreamReader(AbstractTest.this.process.getInputStream()));
AbstractTest.this.output = new BufferedWriter(new OutputStreamWriter(AbstractTest.this.process.getOutputStream()));
}
}
}
protected String write(String data) {
output.write(data + "\n");
output.flush();
return read();
}
protected String read(){
//use input.read() and read until it makes senses
}
protected void tearDown() {
this.process.destroy();
this.process.waitFor();
this.input.close();
this.output.close();
}
}
Afterward, it was pretty easy to make actual test class and implement real test methods.
public void testOption3A(){
start();
String response = write("3");
response = write("733");
assertEquals("*** Cactus ID 733 not found ***",response);
}
Pros
- No refactoring needed
- Actually testing the implementation (No Mocking/Injection)
- Doesn't require any external librairies
Cons
- Pretty hard to debug when things aren't working proprely (Fixable)
- Rely heavily on OS behavior (Windows in this class, but Fixable)
- Compiles the application for every test class (Fixable I think?)
- "Memory Leak" when there is an error and the process is not killed
(Fixable I think?)
This is probably a borderline "hack", but it met my needs and demands.