The way I'd do it is to decouple your application from the console so that you can use fake implementations for printing and reading from the console in your tests. "Fake" is the technical term - you can look up "test doubles" to learn about those and other related ideas. This idea is known as dependency injection, or the dependency inversion principle.
The way we do this is to use interfaces. Here's an example of an application that prints some items:
import java.util.List;
public class ItemPrinterApplication {
public ItemPrinterApplication(OutputWriter outputWriter, List<Item> items) {
this.outputWriter = outputWriter;
this.items = items;
}
public void run() {
outputWriter.writeLine("Name, Price");
items.forEach(item -> outputWriter.writeLine(item.name + ", " + item.price));
}
private OutputWriter outputWriter;
private List<Item> items;
}
OutputWriter
is the thing responsible for the printing. It's just an interface, so the application doesn't know whether it writes to the console or somewhere else:
public interface OutputWriter {
void writeLine(String line);
}
For completeness, the Item
class just holds some data:
public class Item {
public Item(String name, Integer price) {
this.name = name;
this.price = price;
}
public final String name;
public final Integer price;
}
I can then write a test using JUnit that checks that when I run this application, I get the output that I want. I do that by using an implementation of OutputWriter
that just writes to a string. That way it's easy to check in the test:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
public class ItemPrinterTest {
@Test
public void itPrintsAListOfItems() {
List<Item> items =
List.of(
new Item("Apple", 50),
new Item("Carrot", 25),
new Item("Milk", 120)
);
FakeOutputWriter fakeOutputWriter = new FakeOutputWriter();
ItemPrinterApplication app = new ItemPrinterApplication(fakeOutputWriter, items);
app.run();
Assertions.assertEquals(
"Name, Price\n" +
"Apple, 50\n" +
"Carrot, 25\n" +
"Milk, 120\n",
fakeOutputWriter.written
);
}
}
and FakeOutputWriter
looks like
public class FakeOutputWriter implements OutputWriter {
public String written = "";
@Override
public void writeLine(String line) {
written += line;
written += "\n";
}
}
This gives me confidence that I'm writing the output correctly. In main
, though, I want to actually print to the console:
import java.util.List;
public class Main {
public static void main(String[] args) {
OutputWriter outputWriter = new ConsoleOutputWriter();
List<Item> items =
List.of(
new Item("Apple", 50),
new Item("Carrot", 25),
new Item("Milk", 120)
);
new ItemPrinterApplication(outputWriter, items).run();
}
}
and ConsoleOutputWriter
does exactly that:
public class ConsoleOutputWriter implements OutputWriter{
@Override
public void writeLine(String line) {
System.out.println(line);
}
}
You could take the same approach for faking reading input. Your interface would have a function that takes no arguments and reads a string:
interface InputReader {
String readLine()
}
so in the tests you could fake that and in main
, read using a Scanner
or something.