5. Testing in Circus
• Manual tests
• Each developer had its own testing approach
• New elements sometimes broke old elements’ behavior
• Inconsistent behavior across platforms
6. Testing in Circus-HTML5
• Should we use a framework?
• Write them first, while or after writing the code under testing?
• Are there rules, best-practices?
8. Agile testing approach
Unit tests
Acceptance
tests
UI
Tests
Exploratory
tests
Source: The Agile Testing Pyramid
9. Writing unit tests
• Test only one thing
• Only one assert; no and nor or in its name
• Enforce isolation
• 3As: arrange, act, assert
Source: Understanding Test Driven Development
10. Writing unit tests
How would you write the Burn
hits the player test using 3A
(arrange, act, assert)?
PlayerEntity.prototype.burn =
function (damage, ignoreDiversion) {
this.hit(damage, ignoreDiversion);
this.isBurning = true;
this._burningTimer.start(
PlayerEntity.BURNING_TIMEOUT
);
};
11. Writing unit tests
PlayerEntity.prototype.burn =
function (damage, ignoreDiversion) {
this.hit(damage, ignoreDiversion);
this.isBurning = true;
this._burningTimer.start(
PlayerEntity.BURNING_TIMEOUT
);
};
function burnHitsThePlayer() {
// arrange
var player = new PlayerEntity();
this.stub(player, “hit”);
// act
player.burn(1);
// assert
assert.calledOnce(player.hit);
}
12. Writing unit tests
OK, now write the Program
sums two numbers correctly
test
function printSum(num1, num2) {
console.log(num1 + num2);
}
13. There is no secret to writing tests, there are only secrets to
writing testable code!
Misko Hevery in Mr. Testable vs. Mr. Untestable
14. Writing testable code
Usual flaws that hardens testing
1. Do work in constructors
2. Dig into collaborators
3. Having global state and singletons
4. Having classes that do too much
Source: Writing testable code
15. Do work in constructors
How would you create
the GameWorld passes
the correct time value
to the physics
simulation engine test?
var GameWorld = function (entities) {
...
this._gravity = new Vector2D(0.0,-21.2);
this._world = new World(this._gravity);
...
}
GameWorld.prototype.step =
function (delta) {
...
this._world.step(delta / 1000);
...
}
16. Do work in constructors
var GameWorld = function (entities) {
...
this._gravity = new Vector2D(0.0,-21.2);
this._world = new World(this._gravity);
...
}
GameWorld.prototype.step =
function (delta) {
...
this._world.step(delta / 1000);
...
}
function gameWorldPassesTimeCorrectly() {
// arrange
var entities = [];
var gameWorld = new GameWorld(entities);
// act
gameWorld.step(1000);
// assert
... ?
}
17. Do work in constructors
• Have a test specific World
that you could mock?
• Have an accessor to the time
variable in World?
• What if it doesn’t store the
time?
There isn’t a clean
way!
function gameWorldPassesTimeCorrectly() {
// arrange
var entities = [];
var gameWorld = new GameWorld(entities);
// act
gameWorld.step(1000);
// assert
... ?
}
18. Do work in constructors
GameWorld’s constructor has two
responsibilities:
1. Initializing GameWorld
2. Initializing and wiring its dependencies
Solution:
• Split the responsibilities
• Constructor initializes GameWorld
• Factories/builders initializes dependencies
• Inject the dependencies (DI)
var GameWorld = function (entities) {
...
this._finished = false;
this._entities = [];
this._player = null;
this._stage = null;
this._gravity = new Vector2D(0.0, -21.2);
this._world = new World(this._gravity);
...
}
19. Do work in constructors
Before After
var GameWorld = function (entities) {
...
this._finished = false;
this._entities = [];
this._player = null;
this._stage = null;
this._gravity = new Vector2D(0.0, -21.2);
this._world = new World(this._gravity);
...
}
var GameWorld =
function (entities, gravity, world) {
...
this._finished = false;
this._entities = [];
this._player = null;
this._stage = null;
this._gravity = gravity;
this._world = world;
...
}
20. Do work in constructors
var GameWorld =
function (entities, gravity, world) {
...
this._gravity = gravity;
this._world = world;
...
}
GameWorld.prototype.step =
function (delta) {
...
this._world.step(delta / 1000);
...
}
function gameWorldPassesTimeCorrectly() {
// arrange
var entities = [];
var gravity = new Vector2D(0.0, -21.2);
var world = new World(gravity);
var stepSpy = this.spy(world, “step”);
var gameWorld = new GameWorld(entities,
gravity, world);
// act
gameWorld.step(1000);
// assert
assert(stepSpy.calledWith(1));
}
21. Do work in constructors
• Collaborators can’t be mocked
• Unit test has to replicate the work done in the constructor
• Complexity of collaborators is brought to the test
• If one collaborator accesses the network, the test must be executed
with access to the network
• Violates the Single Responsibility Principle (S in SOLID)
• Constructing the object graph is a full-fledged responsibility
22. Do work in constructors
Some signs of the existence of this problem
• The new keyword (except for value objects)
• Static method calls
• Conditional or loop logic
23. Dig into collaborators
How would you create the No
plots are generated if force is
a null vector test?
var TrajectoryPathEntity =
function (gameWorld, referenceEntity) {
Entity.call(this, gameWorld);
this.force = null;
this._gravity = gameWorld.gravity();
this.plots = [];
this._refEntity = referenceEntity;
this._entityMass = this._refEntity.mass();
};
TrajectoryPathEntity.prototype.update =
function (delta) {
this.plots = [];
// Math that computes the points in the
// trajectory and updates this.plots if
// force is valid or non-zero
};
24. Dig into collaborators
var TrajectoryPathEntity =
function (gameWorld, referenceEntity) {
Entity.call(this, gameWorld);
this.force = null;
this._gravity = gameWorld.gravity();
this.plots = [];
this._refEntity = referenceEntity;
this._entityMass = this._refEntity.mass();
};
TrajectoryPathEntity.prototype.update =
function (delta) {
this.plots = [];
// Math that computes the points in the
// trajectory and updates this.plots if
// force is valid or non-zero
};
function
noPlotsAreGeneratedIfForceIsANullVector () {
// arrange
var gravity = new Vector2D(0, -20);
var world = new World(gravity);
var entity = new Entity();
var trajectoryPath = new
TrajectoryPathEntity(world, entity);
trajectory.force = new Vector(0, 0);
// act
trajectory.update(33);
// assert
assert.equals(trajectory.plots.length, 0);
}
25. Dig into collaborators
Trajectory path needs GameWorld only to get
gravity! (also because of Entity’s restriction)
Must create a world when it only need the
gravity
var TrajectoryPathEntity =
function (gameWorld, referenceEntity) {
Entity.call(this, gameWorld);
this.force = null;
this._gravity = gameWorld.gravity();
this.plots = [];
this._refEntity = referenceEntity;
this._entityMass = this._refEntity.mass();
};
TrajectoryPathEntity.prototype.update =
function (delta) {
this.plots = [];
// Math that computes the points in the
// trajectory and updates this.plots if
// force is valid or non-zero
};
function
noPlotsAreGeneratedIfForceIsANullVector () {
// arrange
var gravity = new Vector2D(0, -20);
var world = new World(gravity);
var entity = new Entity();
var trajectoryPath = new
TrajectoryPathEntity(world, entity);
trajectory.force = new Vector(0, 0);
// act
trajectory.update(33);
// assert
assert.equals(trajectory.plots.length, 0);
}
26. Dig into collaborators
Complexity of World is brought to
the test
• Construction of World is made in
TrajectoryPath’s test
• Change in World -> change in
TrajectoryPath’s test
• Resources used by World must
exist in TrajectoryPath’s test
Solution:
• “Don’t look for things, ask for
things”
• Inject the dependency (DI)
function
noPlotsAreGeneratedIfForceIsANullVector () {
// arrange
var gravity = new Vector2D(0, -20);
var world = new World(gravity);
var entity = new Entity();
var trajectoryPath = new
TrajectoryPathEntity(world, entity);
trajectory.force = new Vector(0, 0);
// act
trajectory.update(33);
// assert
assert.equals(trajectory.plots.length, 0);
}
27. Dig into collaborators
Before After
var TrajectoryPathEntity =
function (gameWorld, referenceEntity) {
Entity.call(this, gameWorld);
this.force = null;
this._gravity = gameWorld.gravity();
this.plots = [];
this._refEntity = referenceEntity;
this._entityMass = this._refEntity.mass();
};
var TrajectoryPathEntity =
function (gravity, referenceEntity) {
Entity.call(this);
this.force = null;
this._gravity = gravity;
this.plots = [];
this._refEntity = referenceEntity;
this._entityMass = this._refEntity.mass();
};
28. Dig into collaborators
Before After
function
noPlotsAreGeneratedIfForceIsANullVector () {
// arrange
var gravity = new Vector2D(0, -20);
var world = new World(gravity);
var entity = new Entity();
var trajectoryPath = new
TrajectoryPathEntity(world, entity);
trajectory.force = new Vector(0, 0);
// act
trajectory.update(33);
// assert
assert.equals(trajectory.plots.length, 0);
}
function
noPlotsAreGeneratedIfForceIsANullVector () {
// arrange
var gravity = new Vector2D(0, -20);
var world = new World(gravity);
var entity = new Entity();
var trajectoryPath = new
TrajectoryPathEntity(gravity, entity);
trajectory.force = new Vector(0, 0);
// act
trajectory.update(33);
// assert
assert.equals(trajectory.plots.length, 0);
}
29. Dig into collaborators
• Violates the Law of Demeter
• Violates the Single Responsibility Principle
• Object becomes a service locator
• Creates a deceitful API
• You say that you need A, when actually you need B that is held by A
30. Dig into collaborators
Some signs of the existence of this problem
• An object named context
• More than one “.” in a call chain
• Having to create mocks that returns mocks in tests
31. Global state & singletons
RopeEntity.prototype.onCollision =
function (collision) {
if (this.hasPlayer())
return;
var player = collision.collidedEntity;
var pivotPoint = player.pivotPoint();
player.stand();
var bottomCenter = player.boundingRect()
.bottomCenter();
this._updateContactPoint(bottomCenter);
this.setFocus(true);
this.grabPlayer(player);
this.setState(RopeEntity.State.Bouncing);
var rect = this.boundingRect();
this.testCombo({ x: rect.center().x, y: 0 },
{ x: pivotPoint.x, y: 0 },
0, rect.width / 2);
};
32. Global state & singletons
This test failed randomly. Can you spot the error?
RopeEntity.prototype.onCollision =
function (collision) {
if (this.hasPlayer())
return;
var player = collision.collidedEntity;
var pivotPoint = player.pivotPoint();
player.stand();
var bottomCenter = player.boundingRect()
.bottomCenter();
this._updateContactPoint(bottomCenter);
this.setFocus(true);
this.grabPlayer(player);
this.setState(RopeEntity.State.Bouncing);
var rect = this.boundingRect();
this.testCombo({ x: rect.center().x, y: 0 },
{ x: pivotPoint.x, y: 0 },
0, rect.width / 2);
};
function playerMakes150PointsWhenCollidesAtCenter() {
// arrange
var builder = new WorldBuilder();
var rope = builder.buildRopeEntity();
var player = builder.buildPlayerEntity();
var event = { collidedEntity: player };
var bonusSpy = this.spy(
builder.getWorld().scoreBoard,
"increaseBonus“
);
var pivotPoint = {
x: this.rope.boundingRect().center().x,
y: this.rope.boundingRect().top
};
this.stub(player, "pivotPoint")
.returns(pivotPoint);
// act
rope.onCollision(this.event);
// assert
assert(this.bonusSpy.calledWith(1.5));
}
33. Global state & singletons
RopeEntity
PlatformEntity
GrabberEntity
-shouldAcceptBonus
+testCombo()
GrabberEntity.prototype.testCombo =
function (base, center, subDistance,
maxDistance) {
if (!GrabberEntity._shouldAcceptBonus) {
GrabberEntity._shouldAcceptBonus = true;
this._comboCounted = true;
return;
}
// do combo computation
};
34. Global state & singletons
RopeEntity
PlatformEntity
GrabberEntity
-shouldAcceptBonus
+testCombo()
Cause
• A global variable that isn’t visible in the
method being tested
• So I’ll have to look at all code paths and
see every possible interaction with every
global variable?
Yeap... ☹
35. Global state & singletons
Our solution: use a setup/
teardown method
A better solution:
• Rewrite in order to remove
the global variable
• These tests can’t be run in
parallel ☹
function
playerMakes150PointsWhenCollidesAtCenter() {
// arrange
...
// act
...
// assert
...
}
function setUp() {
GrabberEntity._shouldAcceptBonus = true;
}
function tearDown() {
GrabberEntity._shouldAcceptBonus = false;
}
36. Global state & singletons
• Requires setup/teardown methods to restore the state
• Forces the developer to know every possible interaction with the
global state
• Makes it impossible to run tests in parallel
• Global state is non-mockable
• Global state creates a deceitful API
• Tells that the class or method that accesses the global state has no
dependencies
• Estabilishes hidden channels between objects
37. Global state & singletons
What about singletons?
A singleton is global state in sheep’s clothing
It feels like a plain class, but it is the same as global state:
• Can be acessed anywhere
• Every variable it holds is no different than a static variable
38. Global state & singletons
Can’t I have a class that must have only a single instance?
Yes, you can, but the singleness should be controlled by the
programmer or framework.
This forces the dependencies to be explicit!
39. Global state & singletons
Some signs of the existence of this problem
• Static fields
• Singletons
• Tests failling randomly
• Tests failling when the order of execution changes
40. Class that does too much
• Similar problem of a constructor that does work
• Becomes harder to test features in isolation
• Often, methods have mixed reponsibilities
• Hard to understand and hard to hand-off
41. Class that does too much
Some signs of the existence of this problem
• Excessive scrolling
• When asked what it does, contains too many “and” in the
answer
• A manager might be a sign that it is a class that does more
than it should
• The God class
42. Summary
Constructor doing work
Why is it negative?
• Testing directly is difficult
• Breaks SRP
How to fix it?
• Breaks responsibilities
• Create factories/builders
• Inject dependencies
Dig into collaborators
Why is it negative?
• Deceipts the user of the API
• Breaks SRP and LoD
How to fix it?
• “Don’t look for things, ask for
things”
• Inject dependencies
43. Summary
Global state & singletons
Why is it negative?
• Deceipts user of the API
• Requires setup/teardown
methods
• Forbis tests to be run in
parallel
How to fix it?
• Inject dependencies
Class that does too much
Why is it negative?
• Hardens testing
• Breaks SRP
How to fix it?
• Break responsibilities
44. What I learned
• Tests gave me confidence that my code works
• Brought me tranquility when I needed to make changes
• It is often uncomfortable to write tests
45. What I learned
• Tests are executable documentation
If somehow all your production code got deleted, but you had a backup
of your tests, then you'd be able to recreate the production system
with a little work. (…) If, however, it was your tests that got deleted,
then you'd have no tests to keep the production code clean. The
production code would inevitably rot, slowing you down.
Test First
• There is a relation between testable code and good quality
code
46. Takeaways
• The Factory Pattern makes sense: dependency graph creation
is a full-fledged responsibility
• Dependency Injection and Law of Demeter are basic
buildings blocks of good software design
• Writing tests can help designing software