Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Testing a 2D Platformer with Spock

20,587 views

Published on

Slides from Agile Testing Days Scandinavia. A 40 min talk about how to use the Spock framework to test a simple 2D platformer written in Java.

Published in: Technology
  • Be the first to comment

Testing a 2D Platformer with Spock

  1. 1. Testing a 2D Platformer with Spock Alexander Tarlinder Agile Testing Day Scandinavia 2016
  2. 2. The Why COOL NOT COOL
  3. 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. 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. 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
  6. 6. Maps ▪ Loading ▪ Getting them into the tests Testing Challenges
  7. 7. Sprites & Collisions ▪ Hard to automate ▪ Require visual aids ▪ The owning entity does the physics Testing Challenges
  8. 8. Entity Hierarchy Entity x, y, width, height, (imageId)
 update() BlockBase bump() MovingEntity velocity, direction PlayerGoomba
  9. 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. 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. 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. 12. • 60 FPS • No graphics • State and world setup (aka “test data”) My Initial Fears
  13. 13. About Spock https://github.com/spockframework 2009 2010 2011 2012 2013 2014 2015 2016 0.1 0.7 1.0
  14. 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. 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. 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. 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
  18. 18. Lifecycle Methods Specification scope setupSpec() cleanupSpec() setup() cleanup() def “tested feature”() Test scope @Shared
  19. 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. 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. 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. 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. 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. 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. 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. 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. 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)
  28. 28. Getting Spock apply plugin: 'java'
 
 repositories {
 mavenCentral()
 }
 
 dependencies {
 testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
 }
  29. 29. Spock Reports – Overview
  30. 30. Spock Reports – Details

×