Testing a 2D Platformer with Spock

30
Testing a 2D Platformer with Spock Alexander Tarlinder Agile Testing Day Scandinavia 2016

Transcript of Testing a 2D Platformer with Spock

Testing a 2D Platformer with Spock

Alexander Tarlinder Agile Testing Day Scandinavia

2016

The Why

COOL NOT COOL

▪ 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

[email protected]

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

2D Platformers These Days

• Are made using engines! • Are made up of – Maps – Sprites – Entities & Components – Game loops/update

methodsOut of Scope Today

Real physics Performance Animation Scripting

Maps

▪ Loading ▪ Getting them into the

tests

Testing Challenges

Sprites & Collisions

▪ Hard to automate ▪ Require visual aids ▪ The owning entity

does the physics

Testing Challenges

Entity HierarchyEntity

x, y, width, height, (imageId)update()

BlockBase

bump()

MovingEntity

velocity, direction

PlayerGoomba

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

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}

Assembling with ComponentsPlayer Goomba Flying turtle

InputKeyboard X

AI X XPhysics

Walking X XJumping X

Flying XCD walls X X X

CD enemies XCD bullets X X

GraphicsDraw X X X

Particle effects X

• 60 FPS • No graphics • State and world setup (aka “test data”)

My Initial Fears

About Spock

https://github.com/spockframework

2009 2010 2011 2012 2013 2014 2015 2016

0.1 0.7 1.0

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

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 }

You Can Stack when/thendef "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

You Can Add ands Everywheredef "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

Lifecycle Methods

Specification scope

setupSpec()

cleanupSpec()

setup()

cleanup()

def “tested feature”()

Test scope

@Shared

More Featuresdef "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

Parameterized testsdef "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

Data pipesdef "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()}

Stubsdef "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}

Mocksdef "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.*/(*_)

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.""")

Using Visual Aidsdef "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

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!

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)

Getting Spockapply plugin: 'java' repositories { mavenCentral() } dependencies { testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'}

Spock Reports – Overview

Spock Reports – Details