I have a problem to properly setup a test for a turtle-graphic interface.
As sample I have a simplified interface for just drawing a line.
Now I want to write a test for drawing some grid draw_grid
and ensure that each line of the grid is drawn, but the actual order of drawn lines does not matter. I only need to ensure, that the method calls to move_to
and line_to
are properly paired. I tried this with InSequence
but this leads to several issues, because a couple of line_to
and move_to
calls are used for two lines.
For example move_to(0,0)
is used for the top edge of the grid and for the left border of the first cell. So the test will generate two EXPACT_CALL(sot, move_to(0,0))
in two different sequences, but each of them implicitly with Times(1)
. So I guess this is the main problem. This will happen for each left and top border of a row and similarly for line_to
for right and bottom border of a row.
I also tried to use After
for an EXPACT_CALL
, but this just leads to different test errors.
Is there any nice way to specify the requested behavior? Thanks for your help!
using testing::InSequence;
struct ITurtle
{
virtual void move_to(int x, int y) = 0;
virtual void line_to(int x, int y) = 0;
void line(int x0, int y0, int x1, int y1) { move_to(x0, y0); line_to(x1, y1); }
};
class TurtleMock
: public ITurtle
{
public:
MOCK_METHOD(void, move_to, (int x, int y), (override));
MOCK_METHOD(void, line_to, (int x, int y), (override));
};
void draw_grid(ITurtle &t)
{
for (int r = 0; r < 100; r = 10)
{
// top
t.line(0,r,100,r);
for (int c = 0; c < 100; c = 10)
{ // left
t.line(c,r,c,r 10);
}
// right
t.line(100,r,100,r 10);
}
// bottom
t.line(0,100,100,100);
}
TEST(TurtleTest, lines)
{
TurtleMock sot;
for (int r = 0; r < 100; r = 10)
{
{
InSequence s;
EXPECT_CALL(sot, move_to(0, r));
EXPECT_CALL(sot, line_to(100, r));
}
for (int c = 0; c < 100; c = 10)
{
InSequence s;
EXPECT_CALL(sot, move_to(c,r));
EXPECT_CALL(sot, line_to(c,r 10));
}
{
InSequence s;
EXPECT_CALL(sot, move_to(100, r));
EXPECT_CALL(sot, line_to(100, r 10));
}
}
{
InSequence s;
EXPECT_CALL(sot, move_to(0, 100));
EXPECT_CALL(sot, line_to(100, 100));
}
draw_grid(sot);
}
EDIT:
Extension to proposed solution of Sedenion to support polygon drawing with one initial move_to
followed by a sequence of line_to
statements.
void move_to(int x, int y) final
{
m_move_to_data = {x, y};
}
void line_to(int x, int y) final
{
ASSERT_TRUE(m_move_to_data.has_value());
auto const [x0, y0] = *m_move_to_data;
line_mock(x0, y0, x, y);
m_move_to_data = {x, y};
}
CodePudding user response:
Consider the following simplified example of drawing just 2 lines (live example):
void draw_two_lines(ITurtle &t)
{
t.line(0, 0, 10, 0);
t.line(0, 0, 0, 10);
}
TEST(TurtleTest, two_lines)
{
TurtleMock sot;
{
InSequence s;
EXPECT_CALL(sot, move_to(0, 0)); // (2)
EXPECT_CALL(sot, line_to(10, 0));
}
{
InSequence s;
EXPECT_CALL(sot, move_to(0, 0)); // (1)
EXPECT_CALL(sot, line_to(0, 10));
}
draw_two_lines(sot);
}
Forgetting about InSequence
for a moment, expectations are matched in reverse order (from bottom to top) by default by Google Mock.
Quote from the manual:
By default, when a mock method is invoked, gMock will search the expectations in the reverse order they are defined, and stop when an active expectation that matches the arguments is found (you can think of it as “newer rules override older ones.”).
Now, here we do have InSequence
, i.e. two groups. However, the two groups themselves are not "in sequence". That means gMock will match the first call to move_to(0, 0)
with the second group (marked by (1)
in the code above). Thus, afterwards, line_to(0, 10)
is expected but line_to(10, 0)
gets called, resulting in a test failure. If you exchange the order of the two InSequence-groups, the test will pass. However, this is not really worth anything since your goal is to have the order independent.
What you want is basically to specify something like one "atomic" match of all 4 parameters.
I am not aware of any way to directly express this with the InSequence
or After
machinery of GoogleMock. Thus, I propose to take another approach and store the call of move_to
in a temporary variable, and in the call of line_to
take the remembered two values and the two given values to call a dedicated mock function (live example):
struct ITurtle
{
virtual void move_to(int x, int y) = 0;
virtual void line_to(int x, int y) = 0;
void line(int x0, int y0, int x1, int y1) { move_to(x0, y0); line_to(x1, y1); }
};
class TurtleMock : public ITurtle
{
public:
std::optional<std::pair<int, int>> move_to_data;
virtual void move_to(int x, int y) final {
ASSERT_FALSE(move_to_data.has_value());
move_to_data = {x, y};
}
virtual void line_to(int x1, int y1) final {
ASSERT_TRUE(move_to_data.has_value());
auto const [x0, y0] = *move_to_data;
line_mock(x0, y0, x1, y1);
move_to_data.reset();
}
MOCK_METHOD(void, line_mock, (int x0, int y0, int x1, int y1));
};
void draw_two_lines(ITurtle &t)
{
t.line(0, 0, 10, 0);
t.line(0, 0, 0, 10);
}
TEST(TurtleTest, two_lines)
{
TurtleMock sot;
EXPECT_CALL(sot, line_mock(0, 0, 10, 0));
EXPECT_CALL(sot, line_mock(0, 0, 0, 10));
draw_two_lines(sot);
}
This allows to specify all 4 parameters in one "atomic" match, making the whole stuff with InSequence
unnecessary. The above example passes regardless of the order of the line()
calls in draw_two_lines()
.
Your test for draw_grid()
would then become (live example):
TEST(TurtleTest, grid)
{
TurtleMock sot;
for (int r = 0; r < 100; r = 10)
{
EXPECT_CALL(sot, line_mock(0, r, 100, r));
for (int c = 0; c < 100; c = 10) {
EXPECT_CALL(sot, line_mock(c, r, c, r 10));
}
EXPECT_CALL(sot, line_mock(100, r, 100, r 10));
}
EXPECT_CALL(sot, line_mock(0, 100, 100, 100));
draw_grid(sot);
}
Note: This solution assumes that you cannot or do not want to make ITurtle::line()
virtual. If it were, you could of course ditch the helper move_to_data
and line_mock()
and instead mock line()
directly.