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
):
-
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
roundCompleted
method. 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.gamemonitor
sub-package) that matches the above data format (all values areString
s). Hard-code theplayerName
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; }
-
Generate getter methods for the above fields.
-
Add a static method named
from
that takes aGame
parameter 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 theGame
into aGameView
DTO. -
Create an
HttpGameMonitor
class that implements theGameMonitor
interface (also in theadapter.out.gamemonitor
sub-package).The
roundCompleted
method should convert the incomingGame
parameter to a DTO and then call thepost
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); }
-
Change the
createGame
method in theBlackjackGameApplication
so that an instance of yourHttpGameMonitor
is passed in to theGame
's constructor.WebConfigurationTest
Uses the Real ThingIf 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 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()); } }
Game
instead 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!