Skip to content

Latest commit

 

History

History
590 lines (427 loc) · 12.2 KB

london-school.md

File metadata and controls

590 lines (427 loc) · 12.2 KB
title author date css
London school of test-driven development
Zeger Hendrikse
2023-09-29
css/neon.css

Coders should test

Testers should code

We all should do TDD!

by Zeger Hendrikse


Goals

 

 

 


Don't mock what you don't own

Use adapters instead!

Effective unit tests only test one thing. To do this you have to move the irrelevant portions out of the way (e.g., MockObjects). This forces out what might be a poor design choice.


Test doubles

  • Dummy: a filler, i.e. not really used
  • Fake: fake implementation, e.g. in-mem DBs
  • Stubs: canned answers (for queries)
  • Spies: in-between objects that record call info (commands)
  • Mocks: objects that verify behaviour

Rulez of the TDD game

Red Green Refactor
  1. Write a failing test
  2. Make it pass
  3. Refactor relentlessly

Rulez of the TDD game

Small increments, so we are not allowed to write

  1. any code unless it is to make a failing test pass
  2. any more of a test than is sufficient to fail (also compilation!)
  3. any more code than is sufficient to pass the one failing unit test
---

Kent Beck

Kent Beck


  1. Passes the tests
  2. Reveals intention (Clean code)
  3. No duplication (DRY)
  4. Fewest elements (Simplest thing that could possibly work)

User story / epic

Audio player

now ᴘʟᴀʏɪɴɢ: MyGreatSong.mp3 ───────────⚪────── ◄◄⠀▐▐⠀►► 𝟸:𝟷𝟾 / 𝟹:𝟻𝟼⠀───○ 🔊

 
As a music lover

I want to play my favourite playlist(s)

so that I can use the music during my workouts


Plans are worthless ...

... but planning is essential:

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

 

Credits to Kent Beck and Eisenhower!

What the result will be

<iframe width="100%" height="500" src="//jsfiddle.net/zhendrikse/bu7tv1kp/3/embedded/js,result/dark/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>

Jasmine prerequisites

beforeEach(function() {
  tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);

  tape.play();
  tape.pause();
  tape.rewind(0);
});

Jasmine prerequisites

  • Mocking return value
my_object.my_method.and.returnValue("my_return_value")
  • Mocking subsequent return values
my_object.my_method.and.returnValues("my_return_value1", "my_return_value2")

Jasmine prerequisites

  • Calls with arguments

    spyOn(componentInstance, 'myFunction')
        .withArgs(myArg1).and.returnValue(myReturnObj1)
        .withArgs(myArg2).and.returnValue(myReturnObj2);
  • Verify behaviour

    expect( foo.callMe ).toHaveBeenCalled();

Let's do this

<iframe frameborder="0" width="100%" height="500px" src="https://replit.com/@zwh/tdd?lite=false"></iframe>

Player in initial state

describe('Given a just switched on audioplayer', function () {
  it('should show the first song in the playlist on display', function() {
    audioplayer = new AudioPlayer()
    expect(
      audioplayer.getCurrentSong()).toEqual("play: MyGreatSong.mp3")
  })
})

class AudioPlayer {
  getCurrentSong() {
    return "play: MyGreatSong.mp3"
  }
}

Is the song playing?

it('should have the play/pause button in state play', function(){
  audioplayer = new AudioPlayer()

  expect(
    audioplayer.getPlayPauseButtonStatus()).toEqual(PlayPauseButton.PLAY)
})

const PlayPauseButton = {
	PLAY: "play",
	PAUSE: "pause",
}

class AudioPlayer {
  getCurrentSong() {
    return "play: MyGreatSong.mp3"
  }

  getPlayPauseButtonStatus() {
    return PlayPauseButton.PLAY
  }
}

Red, green, ..., refactor!

describe('Given a just switched on audioplayer', function () {
  beforeEach(function () {
    audioplayer = new AudioPlayer()
  })

  it('should show the first song in the playlist on display', function() {
    expect(
      audioplayer.getCurrentSong()).toEqual("play: MyGreatSong.mp3")   
    })

  it('should have the play/pause button in state play', function(){
    expect(audioplayer.getPlayPauseButtonStatus()).toEqual(PlayPauseButton.PLAY)
  })
})

Our updated plan

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

Previous track button

describe('When the previous track button is pressed', function() {
  it('should do nothing', function() {
      spyOn(audioplayer, "previousTrack")
      audioplayer.previousTrack()
      expect(audioplayer.previousTrack).toHaveBeenCalled();
  })
})

  previousTrack() {}

Our updated plan

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

Next track button

describe('When the next track button is pressed', function () {
  it('should show the next song in the playlist on display', function () {
    audioplayer.nextTrack()
    expect(audioplayer.getCurrentSong()).toEqual("play: MyUpbeatSong.mp3")
  })
})

class AudioPlayer {
  constructor() {
    this.currentTrack = "MyGreatSong.mp3"
  }

  getCurrentSong() {
    return "play: " + this.currentTrack
  }

  getPlayPauseButtonStatus() {
    return PlayPauseButton.PLAY
  }

  previousTrack() {}

  nextTrack() {
    this.currentTrack = "MyUpbeatSong.mp3"
  }
}

Our updated plan

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

Next track once more

describe('When the next track button is pressed twice', function () {
  it('should show the third song in the playlist on display', function () {
    audioplayer.nextTrack()
    audioplayer.nextTrack()
    expect(audioplayer.getCurrentSong()).toEqual("play: MyWorkoutSong.mp3")
  })
})

Make the test pass!

class AudioPlayer {
  constructor() {
    this.currentTrack = "MyGreatSong.mp3"
    this.songCounter = 0
  }

  // ...

  nextTrack() {
    this.songCounter++

    if (this.songCounter == 1)
      this.currentTrack = "MyUpbeatSong.mp3"
    else
      this.currentTrack = "MyWorkoutSong.mp3"
  }
}

Refactor...

Playlist collaborator inevitable

Introduce mock in small increments!

playlist = jasmine.createSpyObj('playlist', ['getCurrentTrack', 'nextTrack'])
playlist.getCurrentTrack.and.returnValue("MyGreatSong.mp3")

audioplayer = new AudioPlayer(playlist)

Step 1

class AudioPlayer {
  constructor(myPlaylist) {
    this.playlist = myPlaylist
    this.currentTrack = this.playlist.getCurrentTrack()
    this.songCounter = 0
  }

Run unit tests ...

Step 2

  nextTrack() {
    this.songCounter++

    if (this.songCounter == 1)
      this.currentTrack = this.playlist.getNextTrack()
    else
      this.currentTrack = this.playlist.getNextTrack()
  }

Run unit tests ...


Step 3

  nextTrack() {
     this.currentTrack = this.playlist.getNextTrack()
  }

Run unit test ...


Step 4

Grouping the double button presses

  1. Promote the playlist to a global var playlist
  2. Move the playlist.getNextTrack.and.returnValues to the double-press tests
  3. Introduce a new beforeEach for the double press tests

To become ... (next slide)


describe('Given a next track button command', function () {
  beforeEach(function () {
    playlist.getNextTrack.and.returnValues("MyUpbeatSong.mp3", "MyWorkoutSong.mp3")
    audioplayer.nextTrack()
  })

  it('should show the next song in the playlist on display', function () {
    expect(audioplayer.getCurrentSong()).toEqual("play: MyUpbeatSong.mp3")
  })

  describe('When the next track button is pressed again', function () {
    it('should show the third song in the playlist on display', function () {
      audioplayer.nextTrack()
      expect(audioplayer.getCurrentSong()).toEqual("play: MyWorkoutSong.mp3")
    })
  })
})

Step 5

Make the test(s) use the playlist too!

it('should show the first song in the playlist on display', function () {
  expect(
    audioplayer.getCurrentSong()).toEqual("play: " + playlist.getCurrentTrack())
})

Our updated plan

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

Pressing next and then previous

describe('When the previous buttons is pressed', function () {
  it('should show the first song in the playlist on display', function () {
    playlist.getPreviousTrack.and.returnValue("MyGreatSong.mp3")
    audioplayer.previousTrack()
    expect(audioplayer.getCurrentSong()).toEqual("play: MyGreatSong.mp3")
  })
})

(Update playlist spy obj method array too!)


previousTrack() {
  this.currentTrack = this.playlist.getPreviousTrack()
}

Our updated plan

  • Audio player in initial state
  • Previous track button
  • Next track button
  • Next track button twice
  • Pressing next and previous

Possible next steps

  • Walk through complete playlist
  • Make the play button work
  • Make the progress bar do its work (difficult!)
  • ...

Retrospective

  • We applied mocks/spies to what we really own!
  • ...
  • ...

References