Create Sequence Tests authored by Merlin's avatar Merlin
Our JUnit tests do a good job of making sure that each part does what the developer expected it to do, but they don't verify end-to-end behaviors. That leaves us at risk for the big picture things not working even though everyone things their part is correct. For example, when a player moves, a command on the client should cause a message to go to the game server that client is playing on and then a message should go from the game server to every other player connected to that server so that everyone can see that you've moved. Because that functionality is spread across machines, JUnit tests cannot verify that executing the one command has all of those effects. You can see how that player movement interaction is tested in MovementBasicSequenceTest and that will be the example for the rest of this explanation.
In order to address that, we developed a system we call "Sequence Tests" where each test starts with a command on one machine and specifies the sequence of messages (between systems) that should result from the execution of that command. The system then emulates every machine involved in the sequence to make sure that its message outputs are correct for the command and messages it receives.
A good engineer who is following TDD should develop a new sequence test any time they are changing the interactions between two or more machines in the system. Here is what that requires:
# Create a class that extends SequenceTest
SequenceTest is an abstract class that defines what a sequence has to have. There are some important things to notice (yes, you should go look at its code) :
- It has two instance variables, interactions and serverList. You're going to have to set those up (don't worry, more instructions are coming).
- It also has two abstract methods:
- resetNecessarySingletons - The system runs through the sequence once for each machine that is involved. This method is called between each of those simulations to reset the machine to the correct initial state. In your subclass, you should call the reset method on any singletons that are involved in your situation.
- setUpMachines - This is called before the test it run and is used to initialize any data that your sequence depends on. Some examples include adding players to the game, putting interactable objects into the game, giving NPCs instructions, etc.
You should build those two methods in your subclass
# Specify the machines that are involved in your sequence
The types of machines are defined in the ServerType enum. In your constructor, add the servers your interaction affects to the serverList instance variable you inherited from SequenceTest. For example, our basic movement test involves three machines: the client of the player who moved, the server that player is connected to, and another player connected to that server. So, we'd put this in the constructor:
```
serverList.add(ServerType.THIS_PLAYER_CLIENT);
serverList.add(ServerType.OTHER_CLIENT);
serverList.add(ServerType.AREA_SERVER);
```
# Specify the commands involved in the sequence
Every sequence starts with the execution of a command, but you can also specify a sequence of commands if necessary. Each command has to be specified and added to the interactions instance variable you inherited. An Interaction specified the command, the initiating player, the machine on which the command would execute, and the sequence of messages that should result from that command (more on this soon). So, for our movement example, the command that we want is:
```
new CommandClientMovePlayer(PlayersForTest.MATT.getPlayerID(),
new Position(PlayersForTest.MATT.getPosition().getRow(),
PlayersForTest.MATT.getPosition().getColumn() + 1))
```
That is saying that Matt moved over one column.
To make that an Interaction and add it to the list, we'd use this:
```
interactions.add(new Interaction(
new CommandClientMovePlayer(PlayersForTest.MATT.getPlayerID(),
new Position(PlayersForTest.MATT.getPosition().getRow(),
PlayersForTest.MATT.getPosition().getColumn() + 1)),
PlayersForTest.MATT.getPlayerID(), ServerType.THIS_PLAYER_CLIENT,
sequence));
```
Note: in this case, the Command already had the player ID in it, but that isn't always the case, so we have to specify it separately in the Interaction.
# Specify the expected sequence of messages
For each Interaction, you specify a list of the messages that you expect would flow around the system as a result of executing that command. In our movement example, we'd expect
- the client to tell the server that the player moved
- the server to tell other clients connected to it that the player moved
There are two types of messages you can specify. First, and most often, messages are "reactions" which means they happen as a result of the command. For these, the system will watch what every machine is outputting and verify that the reaction messages get sent. Second, sometimes you need to specify a more complex situation where some messages should be added to the sequence by the system. In this case, you specify that the message is not a reaction. Then the system will generate that message at that point in the sequence.
For each message, we create a MessageFlow that contains the message, which machine should send it, which machine it should sent to, and whether this message is a reaction or not.
For our basic movement example, this would specify the two messages we expect to see:
```
private final MessageFlow[] sequence =
{new MessageFlow(ServerType.THIS_PLAYER_CLIENT, ServerType.AREA_SERVER,
new PlayerMovedMessage(PlayersForTest.MATT.getPlayerID(),
new Position(PlayersForTest.MATT.getPosition().getRow(),
PlayersForTest.MATT.getPosition().getColumn() + 1)),
true),
new MessageFlow(ServerType.AREA_SERVER, ServerType.OTHER_CLIENT,
new OtherPlayerMovedMessage(PlayersForTest.MATT.getPlayerID(),
new Position(
PlayersForTest.MATT.getPosition().getRow(),
PlayersForTest.MATT.getPosition()
.getColumn() + 1)), true)};
```
# Running your sequence test alone
If you want to run one sequence test, you can use the runnable class RunOneSequenceTest. In it's runSingleTest method, make testClass by your sequence test class. Then running RunOneSequenceTest will run only that one test.
# Adding your test to the suite
If you put your sequence test class in the edu.ship.engr.shipsim.sequencetests package, then RunAllSequenceTests will run it as if it is a JUnit test
\ No newline at end of file