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.

of

Writing testable code Slide 1 Writing testable code Slide 2 Writing testable code Slide 3 Writing testable code Slide 4 Writing testable code Slide 5 Writing testable code Slide 6 Writing testable code Slide 7 Writing testable code Slide 8 Writing testable code Slide 9 Writing testable code Slide 10 Writing testable code Slide 11 Writing testable code Slide 12 Writing testable code Slide 13 Writing testable code Slide 14 Writing testable code Slide 15 Writing testable code Slide 16 Writing testable code Slide 17 Writing testable code Slide 18 Writing testable code Slide 19 Writing testable code Slide 20 Writing testable code Slide 21 Writing testable code Slide 22 Writing testable code Slide 23 Writing testable code Slide 24 Writing testable code Slide 25 Writing testable code Slide 26 Writing testable code Slide 27 Writing testable code Slide 28 Writing testable code Slide 29 Writing testable code Slide 30 Writing testable code Slide 31 Writing testable code Slide 32 Writing testable code Slide 33 Writing testable code Slide 34 Writing testable code Slide 35 Writing testable code Slide 36 Writing testable code Slide 37 Writing testable code Slide 38 Writing testable code Slide 39 Writing testable code Slide 40 Writing testable code Slide 41 Writing testable code Slide 42 Writing testable code Slide 43 Writing testable code Slide 44 Writing testable code Slide 45 Writing testable code Slide 46
Upcoming SlideShare
What to Upload to SlideShare
Next
Download to read offline and view in fullscreen.

0 Likes

Share

Download to read offline

Writing testable code

Download to read offline

Lessons learned from writing tests for The Incredible Circus (Web)

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all
  • Be the first to like this

Writing testable code

  1. 1. Writing testable code Lessons learned from writing tests for The Incredible Circus (Web)
  2. 2. How do you feel... When you are in a huge project and need to change a class that is part of the core of the application?
  3. 3. How I felt...
  4. 4. Testing in Circus
  5. 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. 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?
  7. 7. Traditional testing approach Manual tests through UI Automation suites Unit tests Source: The Agile Testing Pyramid
  8. 8. Agile testing approach Unit tests Acceptance tests UI Tests Exploratory tests Source: The Agile Testing Pyramid
  9. 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. 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. 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. 12. Writing unit tests OK, now write the Program sums two numbers correctly test function printSum(num1, num2) { console.log(num1 + num2); }
  13. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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

Lessons learned from writing tests for The Incredible Circus (Web)

Views

Total views

34

On Slideshare

0

From embeds

0

Number of embeds

1

Actions

Downloads

0

Shares

0

Comments

0

Likes

0

×