3. ▪ Developer (2000→) Java, Perl, C, C++, Groovy, C#, PHP,
Visual Basic, Assembler
▪ Trainer – TDD, Unit testing, Clean Code, WebDriver,
Specification by Example
▪ Developer mentor
▪ Author
▪ Scrum Master
▪ Professional coach
Alexander Tarlinder
https://www.crisp.se/konsulter/alexander-tarlinder
alexander_tar
alexander.tarlinder@crisp.se
4. After This Talk You’ll…
• Know the basics of 2D platformers
• Have seen many features of Spock
• Have developed a sense of game testing
challenges
5. 2D Platformers These Days
• Are made using engines!
• Are made up of
– Maps
– Sprites
– Entities & Components
– Game loops/update
methods
Out of Scope Today
Real physics
Performance
Animation
Scripting
7. Sprites & Collisions
▪ Hard to automate
▪ Require visual aids
▪ The owning entity
does the physics
Testing Challenges
8. Entity Hierarchy
Entity
x, y, width, height, (imageId)
update()
BlockBase
bump()
MovingEntity
velocity, direction
PlayerGoomba
9. Game Loop And Update Method
WHILE (game runs)
{
Process input
Update
Render scene
}
React to input
Do AI
Do physics
▪ Run at 60 FPS
▪ Requires player input
Testing Challenges
10. The Component Pattern –
Motivation
player.update() {
Process movement
Resolve collisions with the world
Resolve collisions with enemies
Check life
…
Move camera
Pick an image to draw
}
11. Assembling with Components
Player Goomba Flying turtle
Input
Keyboard X
AI X X
Physics
Walking X X
Jumping X
Flying X
CD walls X X X
CD enemies X
CD bullets X X
Graphics
Draw X X X
Particle effects X
12. • 60 FPS
• No graphics
• State and world setup (aka “test data”)
My Initial Fears
14. Basic Spock Test Structure
def "A vanilla Spock test uses given/when/then"() {
given:
def greeting = "Hello"
when:
def message = greeting + ", world!"
then:
message == "Hello, world!"
}
Proper test name
GWT
Noise-free assertion
15. A First Test
@Subject
def physicsComponent = new PhysicsComponent()
def "A Goomba placed in mid-air will start falling"() {
given: "An empty level and a Goomba floating in mid-air"
def emptyLevel = new Level(10, 10, [])
def fallingGoomba = new Goomba(0, 0, null)
when: "Time is advanced by two frames"
2.times { physicsComponent.update(fallingGoomba, emptyLevel) }
then: "The Goomba has started falling in the second frame"
fallingGoomba.getVerticalVelocity() >
PhysicsComponent.BASE_VERTICAL_VELOCITY
fallingGoomba.getY() == PhysicsComponent.BASE_VERTICAL_VELOCITY
}
16. You Can Stack when/then
def "A Goomba placed in mid-air will start falling"() {
given: "An empty level and a Goomba floating in mid-air"
def emptyLevel = new Level(10, 10, [])
def fallingGoomba = new Goomba(0, 0, null)
when:
physicsComponent.update(fallingGoomba, emptyLevel)
then:
fallingGoomba.getVerticalVelocity() ==
PhysicsComponent.BASE_VERTICAL_VELOCITY
fallingGoomba.getY() == 0
when:
physicsComponent.update(fallingGoomba, emptyLevel)
then:
fallingGoomba.getVerticalVelocity() >
PhysicsComponent.BASE_VERTICAL_VELOCITY
fallingGoomba.getY() == PhysicsComponent.BASE_VERTICAL_VELOCITY
}
Twice
17. You Can Add ands Everywhere
def "A Goomba placed in mid-air will start falling #3"() {
given: "An empty level"
def emptyLevel = new Level(10, 10, [])
and: "A Goomba floating in mid-air"
def fallingGoomba = new Goomba(0, 0, null)
when: "The time is adanced by one frame"
physicsComponent.update(fallingGoomba, emptyLevel)
and: "The time is advanced by another frame"
physicsComponent.update(fallingGoomba, emptyLevel)
then: "The Goomba has started accelerating"
fallingGoomba.getVerticalVelocity() >
PhysicsComponent.BASE_VERTICAL_VELOCITY
and: "It has fallen some distance"
fallingGoomba.getY() > old(fallingGoomba.getY())
}
You’ve seen this, but forget that you did
And
19. More Features
def "A Goomba placed in mid-air will start falling #4"() {
given:
def emptyLevel = new Level(10, 10, [])
def fallingGoomba = new Goomba(0, 0, null)
when:
5.times { physicsComponent.update(fallingGoomba, emptyLevel) }
then:
with(fallingGoomba) {
expect getVerticalVelocity(),
greaterThan(PhysicsComponent.BASE_VERTICAL_VELOCITY)
expect getY(),
greaterThan(PhysicsComponent.BASE_VERTICAL_VELOCITY)
}
}
With
block
Hamcrest matchers
20. Parameterized tests
def "Examine every single frame in an animation"() {
given:
def testedAnimation = new Animation()
testedAnimation.add("one", 1).add("two", 2).add("three", 3);
when:
ticks.times {testedAnimation.advance()}
then:
testedAnimation.getCurrentImageId() == expectedId
where:
ticks || expectedId
0 || "one"
1 || "two"
2 || "two"
3 || "three"
4 || "three"
5 || "three"
6 || "one"
}
This can be any type of
expression
Optional
21. Data pipes
def "Examine every single frame in an animation"() {
given:
def testedAnimation = new Animation()
testedAnimation.add("one", 1).add("two", 2).add("three", 3);
when:
ticks.times {testedAnimation.advance()}
then:
testedAnimation.getCurrentImageId() == expectedId
where:
ticks << (0..6)
expectedId << ["one", ["two"].multiply(2),
["three"].multiply(3), "one"].flatten()}
22. Stubs
def "Level dimensions are acquired from the TMX loader" () {
final levelWidth = 20;
final levelHeight = 10;
given:
def tmxLoaderStub = Stub(SimpleTmxLoader)
tmxLoaderStub.getLevel() >> new int[levelHeight][levelWidth]
tmxLoaderStub.getMapHeight() >> levelHeight
tmxLoaderStub.getMapWidth() >> levelWidth
when:
def level = new LevelBuilder(tmxLoaderStub).buildLevel()
then:
level.heightInBlocks == levelHeight
level.widthInBlocks == levelWidth
}
23. Mocks
def "Three components are called during a Goomba's update"() {
given:
def aiComponentMock = Mock(AIComponent)
def keyboardInputComponentMock = Mock(KeyboardInputComponent)
def cameraComponentMock = Mock(CameraComponent)
def goomba = new Goomba(0, 0, new GameContext(new Level(10, 10, [])))
.withInputComponent(keyboardInputComponentMock)
.withAIComponent(aiComponentMock)
.withCameraComponent(cameraComponentMock)
when:
goomba.update()
then:
1 * aiComponentMock.update(goomba)
(1.._) * keyboardInputComponentMock.update(_ as MovingEntity)
(_..1) * cameraComponentMock.update(_)
}
This can get creative, like:
3 * _.update(*_)
or even:
3 * _./^u.*/(*_)
24. Some Annotations
• @Subject
• @Shared
• @Unroll("Advance #ticks and expect #expectedId")
• @Stepwise
• @IgnoreIf({ System.getenv("ENV").contains("ci") })
• @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
• @Title("One-line title of a specification")
• @Narrative("""Longer multi-line
description.""")
25. Using Visual Aids
def "A player standing still on a block won't move anywhere"() {
given: "A simple level with some ground"
def level = new StringLevelBuilder().buildLevel((String[]) [
" ",
" ",
"III"].toArray())
def gameContext = new GameContext(level)
and: "The player standing on top of it"
final int startX = BlockBase.BLOCK_SIZE;
final int startY = BlockBase.BLOCK_SIZE + 1
def player = new Player(startX, startY, gameContext, new NullInputComponent())
gameContext.addEntity(player)
def viewPort = new NullViewPort()
gameContext.setViewPort(viewPort)
when: "Time is advanced"
10.times { player.update(); viewPort.update(); }
then: "The player hasn't moved"
player.getX() == startX
player.getY() == startY
}
The level is made
visible in the test
26. def "A player standing still on a block won't move anywhere with visual aids"() {
given: "A simple level with some ground"
def level = new StringLevelBuilder().buildLevel((String[]) [
" ",
" ",
"III"].toArray())
def gameContext = new GameContext(level)
and: "The player standing on top of it"
final int startX = BlockBase.BLOCK_SIZE;
final int startY = BlockBase.BLOCK_SIZE + 1
def player = new Player(startX, startY, gameContext, new NullInputComponent())
gameContext.addEntity(player)
def viewPort = new SwingViewPort(gameContext)
gameContext.setViewPort(viewPort)
when: "Time is advanced"
10.times { slomo { player.update(); viewPort.update(); } }
then: "The player hasn't moved"
player.getX() == startX
player.getY() == startY
}
A real view port
Slow down!
27. Conclusions
• How was Spock useful?
– Test names and GWT labels really helped
– Groovy reduced the bloat
– Features for parameterized tests useful for some tests whereas mocking and
stubbing remained unutilized in this case
• Game testing
– The world is the test data - so make sure you can generate it easily
– Conciseness is crucial - because of all the math expressions
– One frame at the time - turned out to be a viable strategy for handling 60 FPS in
unit tests
– Games are huge state machines - virtually no stubbing and mocking in the core code
– The Component pattern - is more or less a must for testability
– Use visual aids - and write the unit tests so that they can run with real viewports
– Off-by-one errors - will torment you
– Test-driving is hard - because of the floating point math (the API can be teased out,
but knowing exactly where a player should be after falling and sliding for 15
frames is better determined by using an actual viewport)