Skip to content

Characterizing the Game

Do This First...

Be sure to have completed the prior labs before starting. You can also use the solution from the repository on GitHub and do a checkout of the tag lab4-start.

git checkout lab4-start

Goal

In this lab we'll "inject" an object into Game that will collect all of the output into one large String. This will allow us to do some minimal characterization testing to support our major refactoring in the next lab.

We'll first replace usages of System.out.println with a single method, which will then allow us to capture the output in tests. Then we'll refactor usages of the Scanner so we can control the input from the tests as well. Finally, we'll write some tests!

A. Funnel Output

The first step is to replace all calls to System.out.println with a call to a single method (e.g., println). We'll know we've done this correctly if we comment out the implementation of that method, run the game, and don't see any output. If we do see output, it means we've missed something.

Search & Replace

The most straightforward way is to:

  1. Create a static method like void println(Object toPrint)1 that takes an object and does System.out.println with it.

  2. Create a replacement for System.out.print (print without a new line).

  3. Search for System.out. and replace it with nothing, so it calls our new methods2.

  4. You may need an additional method, the compiler will tell you what's missing.

Try that first, then run the game manually and make sure everything works as before. (Did you win? 😁)

Finding Missed Output

Now comment out the internals of the println/print methods, and run the game again. Do you see any output?

Since you can't see anything, you'll have to trust that the input is being processed and do the following:

Press Enter (starts the game and deals cards)

Then type S Enter (chooses the "Stand" option)

You should see nothing (not even empty lines), except for when you type. The game should end and return to the command prompt. If you do see anything, you've missed replacing a System.out.print or System.out.println somewhere, so fix it and try again.

Refactoring

While Search & Replace is a well-known way to go, you know I'm going to have you do it again using automated refactorings. 😉

Reset to the Start

Be sure to go back to the lab4-start tag, or otherwise get yourself back to the starting point before the Search & Replace.

  1. Find a usage of System.out (in a static method is best) that is in a static method, and highlight it:

    highlight-system-out.png

  2. Perform the Introduce Field3 refactoring and name it consoleOut❶. Be sure to select options to initialize it in the Field Declaration❷, do not make it final❸, and replace all occurrences❹. The resulting field should be static, if not, you need to make sure you've selected System.out from a method that is static.

    extract-field.png

As before, run the game manually and it should work as it did before the refactoring. (Did you win this time? 😊)

Finding Missed Output

This time we can't just comment out a method, nor replace our field with null. Instead, we replace it with something that doesn't print any output on the console4. Replace the field assignment:

private static PrintStream consoleOut = System.out;

with:

private static PrintStream consoleOut = 
        new PrintStream(new ByteArrayOutputStream()); // (1)!
  1. 👀   We'll see this code again in a bit!

As before, run the game manually, press Enter, S, and then Enter. You won't see any output, but it should end normally. (You'll never find out if you won or lost, sorry. 😔)

Restore System.out

Be sure to restore the field to use System.out, we'll need it for manual testing. Later on we'll make it replaceable from tests.

B. Feeding the Scanner

Let's write a little test that will eventually become our Characterization Test for Game. We'll put this test in the console package, as that's where it will eventually live, given that it's testing console output.

package com.r2ha.blackjack.adapter.in.console;

import com.r2ha.blackjack.domain.Game;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class GameDisplayTest {

    @Test
    void gamePlays() {
        // Starts the game with an empty String array for the arguments.
        Game.main(new String[0]);
    }

}

Predict

Before you run the test, predict what will happen.

Now run the test.

As you likely predicted, the test hangs, because the game is waiting for keyboard input when calling scanner.nextLine() in the Game.waitForEnterFromUser() method.

Let's make the input something that we can control from our tests.

Consolidate Scanner

There are two Scanner instances—both local variables—in the code. That's bad for multiple reasons: we create a new object every time we want input, but even worse, a Scanner will consume more of its input than it uses, making it hard to control from our test (trust me, I'm saving you hours of troubleshooting right here! 😥).

Your next task is to:

  • Declare a new static field to hold the scanner object
  • Initialize the scanner in the first line of the main method, continuing to use System.in.
  • Remove all local variables of the scanner, using the new static field

This is a slightly risky change, but we can (once again) test manually to ensure that we haven't changed any behavior.

Once you've made the changes, play a game or three (I won't tell anyone) to make sure it still works. (Aren't you glad you're not betting money? 💸)

Replace System.in

Now we can provide input to the game from our test.

Add the following at the top of the GameDisplayTest:

private final InputStream originalSystemIn = System.in;

private void provideInput(String input) {
    byte[] inputBytes = input.getBytes();
    ByteArrayInputStream testIn = new ByteArrayInputStream(inputBytes);
    System.setIn(testIn); // reading from System.in will consume our input
}

@AfterEach
public void restoreSystemInput() {
    System.setIn(originalSystemIn);
}

Then insert the following line at the top of the gamePlays() test method:

provideInput("\nS\n"); // simulates typing: Enter, S, Enter

Now, if you run the test, it should complete (and pass, since we're not asserting anything).

Test Failed?

If you see a failure and stack trace similar to this:

java.util.NoSuchElementException: No line found

    at java.base/java.util.Scanner.nextLine(Scanner.java:1656)
    ...

It means you missed replacing one of the usages of scanner. There should only be one place where the Scanner is instantiated: in the top of the main method. All other code (only two places) should use the static scanner field we created.

C. Spying on the Output

Now that all of the display code is funneled into a single method, and we can control the inputs, we can add a way to redirect that output to an object that we can inspect in our tests.

Test First!

Let's update the test to capture the output and start asserting something useful. After we provide the input that will drive the game, let's inject a PrintStream object5. This object will be used instead of System.out (which is also a PrintStream). (The way we create a PrintStream here should ring a bell.) The new lines in the test are highlighted:

@Test
void gamePlays() {
    provideInput("\nS\n");
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    PrintStream printStream = new PrintStream(baos);
    Game.directOutputTo(printStream);
    // Starts the game with an empty String array for the arguments.
    Game.main(new String[0]);

    String output = baos.toString();
    assertThat(output)
            .isEqualTo("");
}
  • Lines 4-5 create our output-capturing PrintStream
  • Line 6 is a new method that you'll have to implement so that Game uses our PrintStream if we tell it to, otherwise it should default to System.out.
  • Line 10 obtains the captured output.
  • Lines 11-12 are the familiar way to start a characterization test: comparing against an empty string so that we can capture the output from the failed test run.

    Use Correct assertThat

    Be sure to add the correct import for the assertThat, as IntelliJ IDEA sometimes suggests the wrong import. I always put this in the import section of my test classes:

    import static org.assertj.core.api.Assertions.*;

Implement directOutputTo

The directOutputTo() method in Game should, as you could guess, direct the output to the specified PrintStream instead of System.out.

Once you've implement it, run the updated test. It should fail like this:

org.opentest4j.AssertionFailedError: 
expected: 
  ""
 but was: 

followed by a bunch of messy text.

D. What to Compare Against?

Think About This First...

What happens if we try to compare the actual output against a copy of a previous test run? (Try it.)

Will that be reliably predictable across all test runs? If not, why not?

Since we're not quite ready to fix the underlying problem (randomness) preventing predictable tests for the entire string, it's your job to figure out what we can reliably predict as well as what's important for us to "characterize".

Writing More Detailed Assertions

You can try asserting 4 cards are displayed twice (once for the initial deal, then once for the final state). This is tricky because of the ANSI escape sequences, so use Java's regex to remove it and replace them with new lines:

replaceAll(
 "\u001B\\[[\\d;]*[^\\d;]",
 "\n")

Writing Assertions

You'll want to compare against several individual strings, so you can use AssertJ's .containsIgnoringWhitespaces method where you can specify several strings, and any whitespace (including new lines) will be ignored. For example:

assertThat(output)
       .containsIgnoringWhitespaces(
               "Hit [ENTER]",
               "[H]it or [S]tand?");

Now go and write the assertions.

Success

Don't proceed until you feel like you have the text output well-covered and all tests are passing.

Now that we have a reasonable Characterization test, we can feel more comfortable about ripping the Game class apart into the Domain (I/O-free) and Console (I/O-based) pieces. But that will have to wait until the next lab.


You're Done

That's the end of this lab. Well done!

If you want to compare your solution with mine, you can checkout from the repository on GitHub using the tag lab4-solution.

git checkout lab4-solution

  1. We use Object here because the ansi() method that the game uses returns an Ansi object. When we use System.out.println(), it implicitly calls .toString() on the object, so we want to do the same thing. 

  2. You can also do this with the multi-caret functionality by selecting a System.out. that you want to replace, and then pressing Ctrl+G (Mac) or Alt+J (Win/Linux) multiple times to select the rest. Then hit Del

  3. For Mac, it's Cmd+Option+F. On Windows/Linux, it's Ctrl+Alt+F

  4. This is the Null Object design pattern, replacing an object with an object that has the same type, but does nothing. In this case, it does do something (stores the output internally), but we won't see it, so it's effectively doing "nothing". (If output is stored, but never seen, did it really happen? 🤔) 

  5. Wait, don't you hate mocks and spies and such? I don't hate them, I just find them to be often used incorrectly (or too much). In this situation, we're dealing with "legacy code" that we want to change as little as possible as we add characterization tests. Once we've refactored, the injected spy might go away, or we might convert it into a "nullable", specifically an Output Tracker