Skip to content

Game Monitor Service

Now that we can play a game via the browser, let's broadcast the outcome to a central "monitoring" service that already exists.

The "Game Monitor" service will show the final results of the game that was played, and not a game that is still in-progress.

Goal

Using the Port concept from Hexagonal Architecture, we'll create an interface to define how the information will be sent to the external service.

A. Port: The Ideal Interface

Create a new interface that will define our ideal way to communicate with the external service. Add the following interface to the com.jitterted.ebp.blackjack.domain.port package:

public interface GameMonitor {
  void roundCompleted(Game game);
}

An implementation will be passed in to the Game class, so modify its constructor to accept a GameMonitor reference. Since we already have a constructor that takes a parameter, we'll create a two-argument constructor.

// add to Game.java
private GameMonitor gameMonitor;

public Game(Deck deck, GameMonitor gameMonitor) {
  // assign Deck & GameMonitor to private final fields
}
  • Think about: What the code will do if no GameMonitor is specified?

B. Test Using a Mockito Spy

Create a new test class, GameMonitorTest and add these import statements:

// use these static imports in the test class 
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

Then you'll write several tests (like the one shown below) to validate that the roundCompleted method gets called when the game is completed with different scenarios.

To write a test, create a spy and inject it into the Game as in the test below:

@Test
public void playerStandsThenGameIsOverAndResultsSentToMonitor() throws Exception {
    // create the spy based on the interface
    GameMonitor gameMonitorSpy = spy(GameMonitor.class);
    // TODO: finish the rest of this setup...
    Game game = new Game(/* pass in a deck and the gameMonitorSpy */);
    game.initialDeal();  

    //
    // TODO: execute the behavior that you expect to call the GameMonitor
    //

    // verify that the roundCompleted method was called with any instance of a Game class
    verify(gameMonitorSpy).roundCompleted(any(Game.class));
}

Fill-in the code for the TODO (use the Game constructor that takes a GameMonitor as an argument) with the appropriate scenario, watch the test fail, and then modify the Game code to make it pass.

The four test scenarios to cover are as follows (note that these are similar to the ones found in the GameOutcomeTest):

  1. The Player Stands and the game results are sent to the Game Monitor. (When the Player Stands, the game is over.)

  2. The Player Hits and Goes Bust and the game results are sent to the Game Monitor. (When the Player goes Bust, the game is over.)

  3. A negative test where the Player Hits, but does Not Go Bust: no call is made to the roundCompleted method. Since the Player is still playing, no results are sent yet. Use the never() verification as follows:

    verify(gameMonitorSpy, never()).roundCompleted(any(Game.class));
    
  4. (OPTIONAL) The Player is Dealt Blackjack upon the initial deal, the game is over and the results are sent to the Game Monitor.

Once all the tests are passing, you can request the URI for the next step below.


Stop here and wait for the URI.


C. Concrete GameMonitor Implementation

Your goal in this step is to implement an HTTP concrete implementation that sends the game's result as JSON that looks like this:

{
    "playerName": "Joe",
    "outcome": "You busted",
    "playerHandValue": "23",
    "dealerHandValue": "17"
}
  1. Create a DTO class named GameResultDto (in the adapter.out.gamemonitor sub-package) that matches the above data format (all values are Strings). Hard-code the playerName property to be your name. It will look like the following:

    public class GameResultDto {
        private final String playerName;
        private final String outcome;
        private final String playerHandValue;
        private final String dealerHandValue;     
    }
    
  2. Generate getter methods for the above fields.

  3. Add a static method named from that takes a Game parameter and translates the game information and stores the result in the fields. This is similar to the GameView (in the Web Adapter) where the .of(Game) method translates the Game into a GameView DTO.

  4. Create an HttpGameMonitor class that implements the GameMonitor interface (also in the adapter.out.gamemonitor sub-package).

    The roundCompleted method should convert the incoming Game parameter to a DTO and then call the post method (below), with the URI supplied in class, to send the DTO to the remote system. Spring will automatically convert the DTO into JSON for you.

    public void post(String uri, GameResultDto gameResultDto) throws Exception {
      RestTemplate restTemplate = new RestTemplate();
      restTemplate.postForObject(uri, gameResultDto, GameResultDto.class);
    }
    
  5. Change the createGame method in the BlackjackGameApplication so that an instance of your HttpGameMonitor is passed in to the Game's constructor.

    WebConfigurationTest Uses the Real Thing

    If you run the WebConfigurationTest test class, you may notice that results get sent to the Game Monitor. This is because that test class uses the "real" Game created in the BlackjackApplication#createGame() method. To prevent this from happening, you can add the following inner class to the WebConfigurationTest:

    @TestConfiguration
    static class GameTestConfiguration {
        @Primary
        @Bean
        public Game createTestGame() {
            return new Game(new Deck());
        }
    }
    
    The test will then use this configuration for creating an instance of Game instead of the "production" instance.

  6. Play a game and watch the results show up on the GameMonitor web page at https://blackjack-game-monitor.herokuapp.com/.

D. (optional) Create Outcome Translator

Instead of sending the string version of the GameOutcome enum in the DTO, create a translator method that converts the enum to a nicer string. You might want to reuse the one already created for the Console Adapter.


You are done!