This is a problem I have been trying to wrap my head around a couple months now. It has shown up again in a learning project I was working on last night, so I will use that as an example.
I am building an entity-component system, and have a Movement system that controls the movement of each entity on each turn.
class MovementSystem {
private:
vector<PositionComp> positions;
vector<MoveAIComp> moveAIs;
vector<vector<bool> > generateMap()
{
// Cycle through all positions and generates a 2D array
// detailing whether each map space is occupied or not
}
void updateComps(vector<vector<bool> > map)
{
// For each entity generates a 3x3 array from map, centered
// at that entity's position, passes it to
// moveAI.getMove(surroundings), then updates the position
}
public:
void update(Turn turn)
{
map = this->generateMap();
this->updateComps(map);
}
};
Now unit testing some of this functionality is fairly easy. I add a position and moveAI component to the system. The moveAI component is a mock object that returns a specific move that I specify in the test, I then call update on the sytem, and make sure that the position changed in the way is was supposed to.
The problem with this is I have skipped over the entire call to generateMap()
and the first part of updateComps
that creates the 3x3 array. Sure, I have tested that the position is properly updated based on what decision the moveAI makes, but I have not tested that the moveAI is getting the correct information to make that decision.
So far I have come up with a few ways to get around this:
- Change my mock
MoveAIComponent
to cache the inputs togetMove
so I can check them in the test as well. - My
MovementSystem
may be doing too much, so I should split it into two, aMapSystem
that generates the map and updates a newSurroundingsComponent
, then changeMovementSystem
to use the surroundings, position, and moveAI, allowing tests onMapSystem
to cover that functionality. - Leave testing of that functionality for integration tests, when I will be using real
MoveAIComponent
objects that do care about surroundings.
None of these options I particularly like. 1. greatly increases the complexity of both my tests and my mock class(es) since I need to add asserts to ensure the input is correct and cache every call to the mock objects. 2. feels like interface bloat. Since surroundings
is only used inside MovementComponent
, why should I make extra long-lived components to track what is actually temporary, transient, one use data? And finally 3. I dislike due to much the same reasons as 1, except worse. Tests could now fail for two reason: the surroundings
is incorrect when passed to moveAI
or the moveAI
is making an unexpected decision.
So more generally, when you have a class that generates data that is only passed deeper into the stack and is never returned upward, what best way to test that functionality. Of course I am open to suggestions/redesigns that I have not thought of.
So more generally, when you have a class that generates data that is only passed deeper into the stack and is never returned upward
well, something is consuming that data - test whether that is consumed right (or processed correctly).MovementSystem
to depend on the logic inMoveAI
so storing the inputs in a mock object then analyzing would probably be the simplest way to handle that.update
methods would be called. Then I stubbed in tests forMovementSystem
, naming them based on the functionality it would have. Then I implemented the first test, then implemented the logic inMovementSystem
for it to pass, then moved on to the next test. Perhaps that is where I messed up. When I made the first test pass, I implemented all of the generateMap logic which wasn't truly needed