Test-Driven Poolballs¶
Read time: 27 minutes (6963 words)
We looked at the code needed to get your simulation running, but we did no testing in the process. Let’s do that again, this time with testing in the spotlight.
Where do we even start?
As soon as you start thinking up your new classes, you should be thinking about
tests. In Test-Driven Development
, you write a test and make sure it fails.
Often it will fail because the code you write depends on a class you have not
implemented yet. So, we will skip that first run. Instead, as we build our
class, we will add tests that check our implementation code - before we write
that code!
Testing the Pooltable¶
Let’s write some tests for the Pooltable
class.
As shown before, we will be defining the table dimensions in the constructor for this class. Let’s see that those get set properly:
Here is a first cut at the class header file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Copyright 2019 Roie R. Black
#pragma once
class Pooltable {
private:
int width;
int height;
public:
// constructor
Pooltable(int w, int h);
};
|
Not much here. Immediately, I see a problem from the tester’s viewpoint. If we set the dimensions, how will we figure out if they got set correctly> One solution is to add methods so another part of the program (specifically, they testing code) can access those attributes. C++ provides a way around this issue by allowing you to write classes or functions that you declare as “friends” of the class with private features you want to let them access. I have not figured out how to get Catch to use this scheme properly, so we will just set up accessor methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Copyright 2019 Roie R. Black
#pragma once
class Pooltable {
private:
int width;
int height;
public:
// constructor
Pooltable(int w, int h);
// accessors
int getWidth(void);
int getHeight(void);
};
|
Now we can write our first test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Copyright 2019 Roie R. Black
#include "Pooltable.h"
// constructor
Pooltable::Pooltable(int tw, int th) {
width = tw;
height = th;
}
// accessors
int Pooltable::getHeight(void) {
return height;
}
int Pooltable::getWidth(void) {
return width;
}
|
We need some code in an implementation file to pass this test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Copyright 2019 Roie R. Black
#include "Pooltable.h"
// constructor
Pooltable::Pooltable(int tw, int th) {
width = tw;
height = th;
}
// accessors
int Pooltable::getHeight(void) {
return height;
}
int Pooltable::getWidth(void) {
return width;
}
|
Now, we can try this out:
$ make clean
rm -f demo testapp
$ make
g++ -o demo -framework OpenGL -framework GLUT src/main.o lib/Pooltable.o
$ make test
g++ -o testapp -framework OpenGL -framework GLUT tests/test-pooltable.o tests/test_main.o lib/Pooltable.o
./testapp
===============================================================================
All tests passed (2 assertions in 1 test case)
That was too simple, but it moves us forward, just as we want.
Testing Poolballs¶
Next, we need a basic Poolball
class. The code is very similar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Copyright 2019 Roie R. Black
#pragma once
class Poolball {
private:
float radius;
float xPos, yPos;
float xSpeed, ySpeed;
public:
// constructor
Poolball();
Poolball(float x, float y, float vx, float vy);
// accessors
float getXpos(void);
float getYpos(void);
float getXspeed(void);
float getYspeed(void);
float getRadius(void);
};
|
Now we can write our first test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Copyright 2019 Roie R. Black
#include "catch.hpp"
#include "Poolball.h"
const float X = 50;
const float Y = 60;
const float VX = 2;
const float VY = 3;
TEST_CASE("basic poolball tests", "poolball") {
Poolball ball(X,Y,VX, VY);
REQUIRE(ball.getXpos() == X);
REQUIRE(ball.getYpos() == Y);
REQUIRE(ball.getXspeed() == VX);
REQUIRE(ball.getYspeed() == VY);
REQUIRE(ball.getRadius() == 25.0);
}
TEST_CASE("tets default constructor", "poolball") {
Poolball ball;
REQUIRE(ball.getXpos() == 50.0);
REQUIRE(ball.getYpos() == 60.0);
REQUIRE(ball.getXspeed() == 2.0);
REQUIRE(ball.getYspeed() == 3.0);
REQUIRE(ball.getRadius() == 25.0);
}
|
We need some code in an implementation file to pass this test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // Copyright 2019 Roie R. Black
#include "Poolball.h"
// constructor
Poolball::Poolball() {
radius = 25.0;
xPos = 50;
yPos = 60;
xSpeed = 2;
ySpeed = 3;
}
Poolball::Poolball(float x, float y, float vx, float vy) {
xPos = x;
yPos = y;
xSpeed = vx;
ySpeed = vy;
radius = 25;
}
// accessors
float Poolball::getXpos(void) {
return xPos;
}
float Poolball::getYpos(void) {
return yPos;
}
float Poolball::getXspeed(void) {
return xSpeed;
}
float Poolball::getYspeed(void) {
return ySpeed;
}
float Poolball::getRadius(void) {
return radius;
}
|
Now, we can try this out:
$ make clean
rm -f demo testapp
$ make
g++ -o demo -framework OpenGL -framework GLUT src/main.o lib/Poolball.o lib/Pooltable.o
$ make test
g++ -o testapp -framework OpenGL -framework GLUT tests/test-poolball.o tests/test-pooltable.o tests/test_main.o lib/Poolball.o lib/Pooltable.o
./testapp
===============================================================================
All tests passed (12 assertions in 3 test cases)
Once again, our tests pass. I like seeing that number of tests passed go up!
Array of Poolballs¶
Lets go back and set up an array of Poolballs. We need to modify the header file
for the Pooltable
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Copyright 2019 Roie R. Black
#pragma once
#include "Poolball.h"
const int MAXBALLS = 5;
class Pooltable {
private:
int width;
int height;
Poolball balls[MAXBALLS];
public:
// constructor
Pooltable(int w, int h);
// accessors
int getWidth(void);
int getHeight(void);
float getBallXpos(int i);
float getBallYpos(int i);
// mutators
void placeBalls(void);
};
|
That is a pretty simple change. However, now we face a question: how will we make sure the table now has the correct number of balls to play with, and each one is in the right spot?
We have already tested the default constructor for the Poolball
class, so
they will be properly initialized. C++ automatically calls that constructor for
each ball when it builds the array. The problem with that is simple. Every ball
is in exactly the same location, which is not reasonable. We need to add
another method to the Pooltable
class whose job is to place the balls
somehow on the table. That is a job for the placeBalls
method!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Copyright 2019 Roie R. Black
#include "Pooltable.h"
// constructor
Pooltable::Pooltable(int tw, int th) {
width = tw;
height = th;
}
// accessors
int Pooltable::getHeight(void) {
return height;
}
int Pooltable::getWidth(void) {
return width;
}
void Pooltable::placeBalls(void) {
}
float Pooltable::getBallXpos(int i) {
return balls[i].getXpos();
}
float Pooltable::getBallYpos(int i) {
return balls[i].getYpos();
}
|
Note
example code for the placeBalls
method was shown earlier.
To check that each ball moved properly, we added a few more methods that will let us determine the current location of a specific ball:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Copyright 2019 Roie R. Black
#include "catch.hpp"
#include "Pooltable.h"
const int WIDTH = 500;
const int HEIGHT = 300;
TEST_CASE("basic pooltable tests", "pooltable") {
Pooltable table(WIDTH, HEIGHT);
REQUIRE(table.getWidth() == WIDTH);
REQUIRE(table.getHeight() == HEIGHT);
}
TEST_CASE("test ball array setup" "pooltable") {
Pooltable table(WIDTH, HEIGHT);
for (int i = 0; i<5; i++) {
REQUIRE(table.getBallXpos(i) == 50);
REQUIRE(table.getBallYpos(i) == 60);
}
}
|
Now, we can try this out:
$ make clean
rm -f demo testapp src/main.o lib/Poolball.o lib/Pooltable.o tests/test-poolball.o tests/test-pooltable.o tests/test_main.o
$ make
g++ -c src/main.cpp -o src/main.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -c lib/Poolball.cpp -o lib/Poolball.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -c lib/Pooltable.cpp -o lib/Pooltable.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -o demo -framework OpenGL -framework GLUT src/main.o lib/Poolball.o lib/Pooltable.o
$ make test
g++ -c tests/test-poolball.cpp -o tests/test-poolball.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -c tests/test-pooltable.cpp -o tests/test-pooltable.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -c tests/test_main.cpp -o tests/test_main.o -std=c++11 -Iinclude -Wno-deprecated-declarations
g++ -o testapp -framework OpenGL -framework GLUT tests/test-poolball.o tests/test-pooltable.o tests/test_main.o lib/Poolball.o lib/Pooltable.o
./testapp
===============================================================================
All tests passed (22 assertions in 4 test cases)
So far, all we have done is verify that all balls in the array are sitting in
the same position. The next step should add code to the placeBalls
method,
and test code to confirm that placement.
Proceeding with the development¶
I am not going to show all th steps needed to complete this project, you have seen enough code to explore that on your own.
We do need to talk about the overall test plan though.
Ball Movement¶
Making the balls move it an obvious next step. The table will do that job,
looping over each ball in the array. We can add a move
method to the
Poolball
class, and test that easily.
Bouncing against Walls¶
Adding the logic for testing wall collisions will require setting a ball near each wall in the test code, them moving it and confirming that the required velocity flipped signs.
Checking for Ball to Ball Collisions¶
This one is hard, since there is a lot of math involved. Testing this adequately needs a number of test situations, probably more that we need to include in this project. Still, you should test a few situations.
Remember, testing is to increase your confidence that things are working correctly in your code.
Testing Graphics¶
Notice that we have said nothing about testing the graphics aspects of this project. It is possible to test what an application looks like on the screen. But that kind of testing is WAY beyond this first course. We will not be testing anything other than basic object attributed here.
Controlling the Animation Loop¶
In my testing of the Poolball
simulation, I discovered that the code ran much
faster on my PC than it did on my Mac. Exactly why was a bit of a puzzle until
I started digging into how OpenGL/GLUT actually work.
The glutMainLoop Routine¶
The basic logic of the glutMainLoop
routine looks like this:
void glutMainLoop(void) {
while(true) {
if(content_of_window_has_changed) {
drawScene();
if(keyboard_or_mouse_event) {
handleKey();
if(nothing to do) {
animate();
}
}
}
You provide the three functions defined in this loop.
Each pass through this loop creates something called a “frame”, a term that
dates back to the early days of movies. The “frame rate” is the speed of the
loop in “frames per second”, or fps
. Typical numbers for this speed are in
the 20-30 fps range.
Since we have not done anything to alter how the main loop runs, we do not really know how long it will take to get through the loop until we run the code. Depending on how things are set up on each machine, speeds might be different.
Controlling the speed¶
GLUT provides a way to control things. There is a function in GLUT that we
can call to get the amount of time that has passed since the call to
GlutInit
was called. The value returned is in “milliseconds” (1/1000
seconds).
double start_time;
start_time = glutGet(GLUT_ELAPSED_TIME);
This function can be called whenever we like to see how long it takes to do
something. For example, in out animate
function, we could do this:
void animate(void) {
double start_time, stop_time, duration;
start_time = glutGet(GLUT_ELAPSED_TIME);
;
table.check_collisions(ball);
glutPostRedisplay();
;
stop_time = glutGet(GLUT_ELAPSED_TIME);
duration = stop_time - start_time;
fps = 1.0/duration
cout << fps << endl;
}
Wasting Time¶
Once we know how fast our code is, we can slow things down so they work better.
We pick a desired frame rate (say 60 fps), and calculate how much time we need
to waste to get that rate. A pause_ms
routines has been added to the
library that can use used to delay your code a fixed number of milli-seconds
(1/1000 of a second).
There may be cases where you have so much work to do that you cannot get things done in time. In this case, you are not going to waste any time, and things will just slow down. You might be studying your code to find ways to speed things up in this case.
We can modify the main code in the program to see how things work. Adding an
output statement in the aninate
loop will show you how fast your loop is
actually running. From that, you can add a pause with an appropriate number. I
will let you experiment with that if your animation is running too fast.
There is a problem with this, though. The actual time it takes to get through the code in the animate function may actually be extremely short, far shorter than one millisecond. So, we get a duration of zero, which does not help.
However, if the code is that fast, we can ignore the time it takes to do our work, and simple calculate how long we should take to get through the loop.
If we want a frame rate of something like 60 fps, the amount of time we want to waste in each pass is 1/60 second, which works out to 16.6 ms. Try adding this delay in your loop and see if it works.
Wrapping Up¶
This lecture is incomplete, but shows you how to approach development using testing to drive the process. To complete this adventure, you need to add in the missing methods in the classes specified for this project. The missing code is already in previous lecture notes.