1. Overview
My friend is working on a cool new VR game and he asked me to help with some tips and tricks on how to test it.
PS: I was kidding with the VR part, it will be a console tic-tac-toe game:
public class Player {
public static final int MAX_NUMBER = 100;
public static final int MIN_NUMBER = 10;
private final Scanner scanner = new Scanner(System.in);
public int getNextMove() {
System.out.println("please, type your next move and hit <enter>");
int input = scanner.nextInt();
while(input < MIN_NUMBER || input > MAX_NUMBER) {
System.out.println(String.format("the number must between %d and %d! try again...", MIN_NUMBER, MAX_NUMBER));
input = scanner.nextInt();
}
// other game-related logic here
return input;
}
}
Even though it is possible to do some "hacks" and test this method, ugly tests may be symptoms of bad design - so it's always a good idea to refactor the code first.
2. MVC Violation
Firstly, there is no separation between the model, view, and controller. This leads to a lot of coupling between the game logic and the input that comes through the terminal.
The best way to eliminate his direct coupling is to apply the Dependency Inversion Principle. In this case, let's declare an interface that allows user communication. We'll call it "PlayerView":
public interface PlayerView {
int readNextInt();
void write(String message);
}
The Player object will only know about this interface. At this point, we only depend on our own classes (we no longer use System.out, System.in, Scanner … etc):
public class Player {
public static final int MAX_NUMBER = 100;
public static final int MIN_NUMBER = 10;
private final PlayerView view;
public Player(PlayerView console) {
this.view = console;
}
public int getNextMove() {
view.write("please, type your next move and hit <enter>");
int input = view.readNextInt();
while(input < MIN_NUMBER || input > MAX_NUMBER) {
view.write(String.format("the number must between %d and %d! try again...", MIN_NUMBER, MAX_NUMBER));
input = view.readNextInt();
}
return input;
}
}
3. Dependency Inversion
How do we make it work? Well, to make it work the same as before, we now need to create an implementation of the PlayerView interface and copy the old functionality:
public class ConsoleView implements PlayerView {
private static final Scanner scanner = new Scanner(System.in);
@Override
public int readNextInt() {
return scanner.nextInt();
}
@Override
public void write(String message) {
System.out.println(message);
}
}
As a result, when the game will be initialized, we'll need to create the players based on a ConsoleView, like this:
Player player = new Player(new ConsoleView());
4. Writing a Mock
What are the gains? After this inversion of the dependency, the Player class can be tested with ease. For instance, we can use a mocking library to mock the view and allow us to test the rest of the functionality in isolation. In java, mockito is a popular and powerful tool for this.
On the other hand, blindly using libraries and frameworks might make us lose sight of the bigger image. So, from time to time, it is better to code the solution ourselves instead of bringing in a 3rd party library.
Therefore, let's create a very simple version of a mock object. We can do this simply by writing a new implementation of the PlayerView interface:
public class MockView implements PlayerView {
private List<Integer> mockedUserInputs = new ArrayList<>();
private List<String> displayedMessages = new ArrayList<>();
@Override
public int readNextInt() {
return mockedUserInputs.remove(0);
}
@Override
public void write(String message) {
displayedMessages.add(message);
}
public List<String> getDisplayedMessages(){
return displayedMessages;
}
public void mockedUserInputs(Integer... values) {
mockedUserInputs.addAll(Arrays.asList(values));
}
}
As we can see, we'll use two lists
mockedUserInputs will be specified in the test setup. Each time somebody will call readNextInt(), the mock will return the next value from the list.
displayedMessages only has a getter. This list will be used to store all the messages we are trying to print. This can be useful in case we want to check that we are displaying the messages correctly.
5. Unit Testing
Finally, let's use our tailor-made mock class and write some unit tests:
@Test
void shouldAskUserToEnterNextMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(11);
TestablePlayer player = new TestablePlayer(mockedView);
//when
player.getNextMove();
//then
List<String> displayedMessages = mockedView.getDisplayedMessages();
assertThat(displayedMessages)
.containsExactly("please, type your next move and hit <enter>");
}
@Test
void givenInvalidInput_shouldAskUserToReEnterTheMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(5, 22);
TestablePlayer player = new TestablePlayer(mockedView);
//when
player.getNextMove();
//then
assertThat(mockedView.getDisplayedMessages())
.containsExactly(
"please, type your next move and hit <enter>",
"the number must between 10 and 100! try again...");
}
@Test
void shouldReturnUsersMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(44);
TestablePlayer player = new TestablePlayer(mockedView);
//when
int userMove = player.getNextMove();
//then
assertThat(userMove)
.isEqualTo(44);
}
6. Conclusion
In this article, we've learned about the MVC design pattern. We applied the Dependency Inversion Principle (the "D" in "SOLID") to decouple the controller from the view.
Finally, we were able to test it and we learned how to create a very simple mock object to simulate the user interaction.
Thank You!
Thanks for reading the article and please let me know what you think! Any feedback is welcome.
If you want to read more about clean code, design, unit testing, functional programming, and many others, make sure to check out my other articles.
If you like my content, consider following or subscribing to the email list. Finally, if you consider supporting my blog and buy me a coffee I would be grateful.
Happy Coding!
Top comments (4)
Nice writeup.
Here is a tip for the DEV site. I noticed near the end that you have a link to where you originally posted this. DEV has a feature that allows you to specify the canonical url in a case like this where you cross posted. You can find it in the settings for your post. Most of those who use the canonical link feature cross post here from a personal blog site, but you can specify a canonical to medium, etc as well. The benefit is that (a) it avoids search engines penalizing one or the other in results as duplicate content, and (b) it enables you to specify which one should get the credit in search rankings.
Hello @cicirello, Thanks for reaching out! I found it now and updated :)
You're welcome