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
GameMonitoris 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):
-
The Player Stands and the game results are sent to the Game Monitor. (When the Player Stands, the game is over.)
-
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.)
-
A negative test where the Player Hits, but does Not Go Bust: no call is made to the
roundCompletedmethod. Since the Player is still playing, no results are sent yet. Use thenever()verification as follows:verify(gameMonitorSpy, never()).roundCompleted(any(Game.class)); -
(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"
}
-
Create a DTO class named
GameResultDto(in theadapter.out.gamemonitorsub-package) that matches the above data format (all values areStrings). Hard-code theplayerNameproperty 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; } -
Generate getter methods for the above fields.
-
Add a static method named
fromthat takes aGameparameter and translates the game information and stores the result in the fields. This is similar to theGameView(in the Web Adapter) where the.of(Game)method translates theGameinto aGameViewDTO. -
Create an
HttpGameMonitorclass that implements theGameMonitorinterface (also in theadapter.out.gamemonitorsub-package).The
roundCompletedmethod should convert the incomingGameparameter to a DTO and then call thepostmethod (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); } -
Change the
createGamemethod in theBlackjackGameApplicationso that an instance of yourHttpGameMonitoris passed in to theGame's constructor.WebConfigurationTestUses the Real ThingIf you run the
WebConfigurationTesttest class, you may notice that results get sent to the Game Monitor. This is because that test class uses the "real"Gamecreated in theBlackjackApplication#createGame()method. To prevent this from happening, you can add the following inner class to theWebConfigurationTest:The test will then use this configuration for creating an instance of@TestConfiguration static class GameTestConfiguration { @Primary @Bean public Game createTestGame() { return new Game(new Deck()); } }Gameinstead of the "production" instance. -
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!