title | author | date | css |
---|---|---|---|
London school of test-driven development |
Zeger Hendrikse |
2023-09-29 |
css/neon.css |
- London versus Detroit schools of TDD
- See when mocks should be used in practice
- Practice TDD as a mockist
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.
- 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
|
Small increments, so we are not allowed to write
- any code unless it is to make a failing test pass
- any more of a test than is sufficient to fail (also compilation!)
- any more code than is sufficient to pass the one failing unit test
- Passes the tests
- Reveals intention (Clean code)
- No duplication (DRY)
- Fewest elements (Simplest thing that could possibly work)
now ᴘʟᴀʏɪɴɢ: MyGreatSong.mp3 ───────────⚪────── ◄◄⠀▐▐⠀►► 𝟸:𝟷𝟾 / 𝟹:𝟻𝟼⠀───○ 🔊
As a music lover
<iframe width="100%" height="500" src="//jsfiddle.net/zhendrikse/bu7tv1kp/3/embedded/js,result/dark/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
<iframe frameborder="0" width="100%" height="500px" src="https://replit.com/@zwh/tdd?lite=false"></iframe>
I want to play my favourite playlist(s)
so that I can use the music during my workouts
- 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!
<iframe width="100%" height="500" src="//jsfiddle.net/zhendrikse/bu7tv1kp/3/embedded/js,result/dark/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
- Creating a spy object
beforeEach(function() {
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
tape.play();
tape.pause();
tape.rewind(0);
});
- 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")
-
Calls with arguments
spyOn(componentInstance, 'myFunction') .withArgs(myArg1).and.returnValue(myReturnObj1) .withArgs(myArg2).and.returnValue(myReturnObj2);
-
Verify behaviour
expect( foo.callMe ).toHaveBeenCalled();
<iframe frameborder="0" width="100%" height="500px" src="https://replit.com/@zwh/tdd?lite=false"></iframe>
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"
}
}
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
}
}
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)
})
})
Audio player in initial state✓- Previous track button
- Next track button
- Next track button twice
- Pressing next and previous
describe('When the previous track button is pressed', function() {
it('should do nothing', function() {
spyOn(audioplayer, "previousTrack")
audioplayer.previousTrack()
expect(audioplayer.previousTrack).toHaveBeenCalled();
})
})
previousTrack() {}
Audio player in initial state✓Previous track button✓- Next track button
- Next track button twice
- Pressing next and previous
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"
}
}
Audio player in initial state✓Previous track button✓Next track button✓- Next track button twice
- Pressing next and previous
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")
})
})
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"
}
}
Introduce mock in small increments!
playlist = jasmine.createSpyObj('playlist', ['getCurrentTrack', 'nextTrack'])
playlist.getCurrentTrack.and.returnValue("MyGreatSong.mp3")
audioplayer = new AudioPlayer(playlist)
class AudioPlayer {
constructor(myPlaylist) {
this.playlist = myPlaylist
this.currentTrack = this.playlist.getCurrentTrack()
this.songCounter = 0
}
nextTrack() {
this.songCounter++
if (this.songCounter == 1)
this.currentTrack = this.playlist.getNextTrack()
else
this.currentTrack = this.playlist.getNextTrack()
}
Run unit tests ...
nextTrack() {
this.currentTrack = this.playlist.getNextTrack()
}
Run unit test ...
- Promote the
playlist
to a globalvar playlist
- Move the
playlist.getNextTrack.and.returnValues
to the double-press tests - 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")
})
})
})
it('should show the first song in the playlist on display', function () {
expect(
audioplayer.getCurrentSong()).toEqual("play: " + playlist.getCurrentTrack())
})
Audio player in initial state✓Previous track button✓Next track button✓Next track button twice✓- Pressing next and 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()
}
Audio player in initial state✓Previous track button✓Next track button✓Next track button twice✓Pressing next and previous✓
- Walk through complete playlist
- Make the play button work
- Make the progress bar do its work (difficult!)
- ...
- We applied mocks/spies to what we really own!
- ...
- ...