Tea Time
A Beginner's Guide to JavaScript Testing using Jasmine
By John Bintz
Automated testing is important
Fortunately, we're beyond that nowadays
require 'spec_helper'
describe MyCoolWebsite do
let(:website) { described_class.new }
describe '#cool_method' do
subject { website.cool_method }
let(:oh_yeah) { [ double_cool ] }
let(:double_cool) { 'double cool' }
before do
website.stubs(:whoa_cool).returns(oh_yeah)
end
it { should == double_cool }
end
end
But there's more to web apps than Ruby nowadays...
<img src="normal.gif"
onmouseover="this.src='hover.gif'"
onmouseout="this.src='normal.gif'" />
<script type="text/javascript">
function showMyCoolTitle(title, length) {
if (length == null) { length = 0; }
if (length <= title.length) {
document.title = title.substr(0, length);
length++;
setTimeout(function() { showMyCoolTitle(title, length); }, 75);
}
}
window.onload = function() {
showMyCoolTitle("My cool website! Whoaaaaa!");
}
</script>
Automated testing is important
require 'spec_helper'
describe MyCoolWebsite do
let(:website) { described_class.new }
describe '#cool_method' do
subject { website.cool_method }
let(:oh_yeah) { [ double_cool ] }
let(:double_cool) { 'double cool' }
before do
website.stubs(:whoa_cool).returns(oh_yeah)
end
it { should == double_cool }
end
end
describe 'MyCoolWebsiteView', ->
website = null
beforeEach ->
website = new MyCoolWebsiteView()
describe '#coolMethod', ->
doubleCool = 'double cool'
ohYeah = [ doubleCool ]
beforeEach ->
website.whoaCool = -> ohYeah
it 'should be double cool', ->
expect(website.coolMethod()).toEqual(doubleCool)
BDD unit testing framework for JavaScript
No need to install anything right now
Code under test on the right
Write code in CoffeeScript
Describes a thing or a behavior of a thing
describe 'Cat', ->
# cat behavior descriptions go here
Something that cats do...
describe 'Cat', ->
describe '#meow', ->
# description of the meow behavior goes here
John behavior #1
Use Ruby-style indicators for instance- and class-level methods, even in Jasmine
describe 'John', ->
describe 'spec definitions', ->
it 'should look like you did it in RSpec', ->
Describe how we expect a cat to meow
describe 'Cat', ->
describe '#meow', ->
it 'should meow correctly', ->
# expectation of a cat meowing
We have the description...
Now let's add the expectations!
What should we get as an output?
describe 'Cat', ->
describe '#meow', ->
it 'should meow correctly', ->
expect(cat.meow()).toEqual('meow')
describe 'Cat', ->
describe '#meow', ->
it 'should meow correctly', ->
cat = new Cat()
expect(cat.meow()).toEqual('meow')
# code-under-test
class this.Cat
meow: ->
// safety wrapper to prevent global pollution
(function() {
// ...but we want to pollute the Cat class
this.Cat = (function() {
function Cat() {}
Cat.prototype.meow = function() {};
return Cat;
})();
})(this) // this is window in a browser
1 spec, 1 failure
Expected undefined to equal 'meow'.
class this.Cat
meow: -> "meow"
Here's what you should have meow...
# spec
describe 'Cat', ->
describe '#meow', ->
it 'should meow correctly', ->
expect(cat.meow()).toEqual('meow')
# code-under-test
class this.Cat
meow: -> "meow"
What if the cat meows differently based on certain states?
describe 'Cat', ->
describe '#meow', ->
describe 'hungry', ->
# Cat#meow expectation for when
# the cat is hungry
describe 'going to the vet', ->
# Cat#meow expectation for when
# the cat knows it's vet time
describe 'Cat', ->
describe '#meow', ->
describe 'hungry', ->
it 'should be a mournful meow', ->
cat = new Cat()
cat.state = -> Cat.HUNGRY
# ...just like cat.stubs(:state)
expect(cat.meow()).toEqual("meeeyaow")
describe 'going to the vet', ->
it 'should be an evil meow', ->
cat = new Cat()
cat.state = -> Cat.VET_PSYCHIC
# ...just like the one above
expect(cat.meow()).toEqual("raowwww")
before do
@cat = Cat.new
end
it 'should be a mournful meow' do
@cat.stubs(:state).returns(Cat::HUNGRY)
@cat.meow.should == "meeyaow"
end
before do
@instance_variable = "yes"
end
it "is in same context as before block" do
@instance_variable.should == "yes"
end
beforeEach -> it -> afterEach
beforeEach ->
@instanceVariable = "yes"
it "should be in the same context", ->
expect(@instanceVariable).toEqual("yes")
describe 'Cat', ->
describe '#meow', ->
beforeEach ->
@cat = new Cat()
describe 'hungry', ->
it 'should be a mournful meow', ->
@cat.state = -> Cat.HUNGRY
expect(@cat.meow()).toEqual("meeeyaow")
describe 'going to the vet', ->
it 'should be an evil meow', ->
@cat.state = -> Cat.VET_PSYCHIC
expect(@cat.meow()).toEqual("raowwww")
A little semantics game...
describe 'Cat', ->
describe '#meow', ->
describe 'hungry', ->
# cat codes
describe 'going to the vet', ->
# moar cat codes
This works, but it can be clearer
describe Cat do
describe '#meow' do
describe 'hungry' do
# cat codes
end
describe 'going to the vet' do
# moar cat codes
end
end
end
Description of different states for a test
describe Cat do
let(:cat) { described_class.new }
# save describe for things or behaviors...
describe '#meow' do
subject { cat.meow }
# use context to describe states
context 'hungry' do
# cat codes
end
context 'going to the vet' do
# moar cat codes
end
end
end
Jasmine doesn't have context
this.context = this.describe
this.context = this.describe
describe 'Cat', ->
describe '#meow', ->
context 'hungry', ->
# cat codes
context 'going to the vet', ->
# moar cat codes
this.context = this.describe
describe 'Cat', ->
describe '#meow', ->
beforeEach ->
@cat = new Cat()
context 'hungry', ->
it 'should be a mournful meow', ->
@cat.state = -> Cat.HUNGRY
expect(@cat.meow()).toEqual("meeeyaow")
context 'going to the vet', ->
it 'should be an evil meow', ->
@cat.state = -> Cat.VET_PSYCHIC
expect(@cat.meow()).toEqual("raowwww")
class this.Cat
@HUNGRY = 'hungry'
@VET_PSYCHIC = 'vet psychic'
meow: ->
switch this.state()
when Cat.HUNGRY
"meeeyaow"
when Cat.VET_PSYCHIC
"raowwww"
cat.meow.should == "meow"
cat.should be_a_kind_of(Cat)
cat.should_not be_hungry
# => cat.hungry?.should == false
expect(cat.meow()).toEqual("meow")
expect(cat.prototype).toEqual(Cat.prototype)
expect(cat.isHungry()).not.toBeTruthy()
Lots of built in matchers
toEqual(object)
toBeTruthy()
toBeFalsy()
toBeGreaterThan()
toBeLessThan()
toBeUndefined()
toContain()
toMatch()
expect(cat.isHungry()).not.toBeTruthy()
Create your own matchers!
MyMatchers =
toBeHungry: ->
return @actual.isHungry() == true
beforeEach ->
this.addMatchers(MyMatchers)
describe 'Cat', ->
beforeEach ->
@cat = new Cat()
it 'should not be hungry', ->
expect(@cat).not.toBeHungry()
describe
it
expect
toSomething()
beforeEach
afterEach
No, this isn't a talk about integration testing
Testing the right things in your JavaScript unit tests
John behavior #2
Mock, stub, and spy on anything that should be handled in an integration test
describe 'John', ->
describe 'spec definitions', ->
it 'should keep unit tests as focused as possible', ->
Feature: Cat Behaviors
Scenario: Hungry cats meow a particular way
Given I have a cat
And the cat is hungry
When the cat meows
Then the meow should sound like "meeyaow"
class this.Cat
@FOOD_THRESHOLD = 20
@HUNGRY = 'hungry'
constructor: (@foodLevel = 30) ->
meow: ->
switch this.state()
when Cat.HUNGRY
"meeyaow"
state: ->
if @foodLevel < Cat.FOOD_THRESHOLD
Cat.HUNGRY
describe 'Cat', ->
describe '#meow', ->
context 'hungry', ->
it 'should be a mournful meow', ->
cat = new Cat()
cat.foodLevel = 15
expect(cat.meow()).toEqual("meeeyaow")
A perfectly cromulent test
class this.Cat
meow: ->
switch this.state() # <= dependent code executed
when Cat.HUNGRY
"meeyaow"
Why make your unit tests fragile?
cat.foodLevel = 15
# do we care about food level in this test?
# all we care about is that the cat is hungry
describe 'Cat', ->
describe '#meow', ->
describe 'hungry', ->
it 'should be a mournful meow', ->
cat = new Cat()
cat.state = -> Cat.HUNGRY
# ^^^ we don't care how state works,
# we just want a hungry cat
expect(cat.meow()).toEqual("meeeyaow")
Instance Stubs in JavaScript
Just replace the method on the instance
class this.Cat
state: ->
# cat codes
cat = new Cat()
cat.state = -> "whatever"
Stubs just return something when called
Mocks expect to be called
Test fails if all mocks are not called
Jasmine blurs the line a little
Spies work like mocks, but with additional abilities
class this.Cat
vocalProcessor: (speech) =>
if this.isAirborne()
this.modifyForAirborne(speech)
else
this.modifyForGround(speech)
describe 'Cat#vocalProcessor', ->
speech = "speech"
beforeEach ->
@cat = new Cat()
context 'airborne', ->
beforeEach ->
spyOn(@cat, 'modifyForAirborne')
@cat.isAirborne = -> true
it 'should be modified for flight', ->
@cat.vocalProcessor(speech)
expect(@cat.modifyForAirborne).toHaveBeenCalledWith(speech)
spyOn
replaces a method on an instance with a spy method
spyOn(@cat, 'modifyForAirborne')
Can return a value, run code, run the original code, or just wait to be called
Two basic ways to make sure a spy is called
toHaveBeenCalledWith(args...)
Called least once with the given parameters
expect(@cat.modifyForAirborne).toHaveBeenCalledWith(speech)
toHaveBeenCalled()
Just called, no parameter check
expect(@cat.modifyForAirborne).toHaveBeenCalled()
Instance Mocks/Spies in JavaScript
Use spyOn
/toHaveBeenCalled
matchers
class this.Cat
state: ->
# cat codes
cat = new Cat()
spyOn(cat, 'state')
expect(cat.state).toHaveBeenCalled()
spyOn
works great with class-level stubs and mocks, too
class this.Cat
@generateFurColor: (base) ->
# magicks to make a fur color given a base
regrowFur: (damagedHairs) ->
for follicle in damagedHairs
follicle.regrow(Cat.generateFurColor(this.baseColor))
Cat.generateFurColor = ->
"whoops i nuked this method for every other test"
describe 'Cat#regrowFur', ->
color = 'color'
beforeEach ->
@cat = new Cat()
@follicle =
regrow: ->
@follicles = [ follicle ]
spyOn(Cat, 'generateFurColor').andReturn(color)
# ^^^ original is replaced when done
spyOn(@follicle, 'regrow')
it 'should regrow', ->
@cat.regrowFur(@follicles)
expect(@follicle.regrow).toHaveBeenCalledWith(color)
Class Stubs in JavaScript
Use spyOn
to generate stubs so that the original code is replaced after the test
class this.Cat
@injectPsychicPowers: (cat) ->
# cat codes
spyOn(Cat, 'injectPsychicPowers').andReturn(psychicCat)
John behavior #3
If you have too many mocks/stubs/contexts, your code is too complex
describe 'John', ->
describe 'spec definitions', ->
it 'should obey the Law of Demeter as much as possible', ->
it 'should not smell too funny', ->
describe 'Cat#fetch', ->
object = null
context 'a mouse', ->
beforeEach ->
object = new Mouse()
context 'fast mouse', ->
it 'should wear down the mouse', ->
# who
context 'slow mouse', ->
it 'should deliver a present to you', ->
# cares
context 'a ball', ->
beforeEach ->
object = new Ball()
context 'ball is bouncing', ->
it 'should cause the cat to leap', ->
# this
context 'ball is rolling', ->
it 'should cause the cat to slide on the floor', ->
# test
context 'a red dot', ->
laser = null
beforeEach ->
laser = new Laser()
context 'laser out of batteries', ->
it 'should not activate', ->
# is
context 'laser functioning', ->
it 'should activate, driving the cat insane', ->
# huge and unmaintainable and silly
Sometimes you just need a big blob of unit tests
# fast and focused!
describe 'Cat#respondsTo', ->
beforeEach ->
@cat = new Cat()
context 'successes', ->
it 'should respond', ->
for request in [ 'kitty kitty', 'pookums', 'hisshead' ]
expect(@cat.respondsTo(request)).toBeTruthy()
# slow and synergistic!
Scenario Outline: Successful responsiveness
Given I have a cat
When I call it with "<request>"
Then the cat should respond
Examples:
| request |
| kitty kitty |
| pookums |
| hisshead |
Find what works best for you and stick with it
...until you get sick of it, of course...
Starts a Rack server for running Jasmine against your code
Really easy to plug into an existing Rails project
Want to make that run fast?
Use PhantomJS or jasmine-headless-webkit
Fast code running in a real browser
Some miscellaneous hints and tips
Mocking and stubbing $.fn
calls
this.containerWaiter = ->
$('#container').addClass('wait').append('<div class="waiting" />')
$.fn.makeWait = ->
$(this).addClass('wait').append('<div class="waiting" />')
this
this.containerWaiter = ->
$('#container').makeWait()
describe 'container', ->
beforeEach ->
setFixtures('<div id="container" />')
it 'should make it wait', ->
containerWaiter()
expect($('#container')).toHaveClass('wait')
expect($('#container')).toContain('div.waiting')
describe '$.fn.makeWait', ->
it 'should make wait', ->
$div = $('<div />')
$div.makeWait()
expect($div).toHaveClass('wait')
expect($div).toContain('div.waiting')
describe 'container', ->
beforeEach ->
setFixtures('<div id="container" />')
spyOn($.fn, 'makeWait')
it 'should make it wait', ->
containerWaiter()
expect($.fn.makeWait).toHaveBeenCalled()
No longer testing jQuery, just testing for our code
Animations and other time-dependent things
class Cat
constructor: ->
@mood = "happy"
pet: ->
setTimeout(
-> @mood = "angry"
, 500
)
Do you really need to test the setTimeout
?
class Cat
constructor: ->
@mood = "happy"
pet: -> setTimeout(@makeAngry, 500)
makeAngry: => @mood = "angry"
Use Jasmine's waitsFor
and runs
describe 'cat moods', ->
it 'should change moods', ->
cat = new Cat()
# we want to know the cat's current mood
currentMood = cat.mood
# start petting the cat
runs -> cat.pet()
# wait one second for the cat's mood to change
waitsFor(
->
cat.mood != currentMood
, "Cat changed its mood",
1000
)
# expect the inevitable
runs ->
expect(cat.mood).toEqual('angry')
Underscore.js mixins
and other prototype mixin-style extensions
CatLike =
catify: (name) ->
"meow meow #{name}"
# mix in to the Underscore object
_.mixin(CatLike)
# use it
_.catify("john") # => "meow meow john"
describe '_.catify', ->
it 'should catify', ->
expect(_.catify("hiss")).toEqual("meow meow hiss")
Eliminate the Underscore.js dependency
describe 'CatLike', ->
beforeEach ->
@helper = {}
for method, code of CatLike
@helper[method] = code
describe '#catify', ->
it 'should catify', ->
expect(@helper.catify("hiss")).toEqual("meow meow hiss")
So that's pretty much it.
Basic parts of Jasmine unit tests
describe
it
expect
toSomething()
beforeEach
afterEach
Mocking and stubbing
direct method replacement
spyOn()
toHaveBeenCalled()
toHaveBeenCalledWith()
Running Jasmine in your project
Hints and tips for JavaScript testing