Sie sind auf Seite 1von 60

!

"#$ &'"($)*(
+&+,-. /.,.01123
4567*(8 9*7: ;<==553#'*>7
!"#$%&' )%$* +,--""./0%1$
Copyrght 2012 |ack Frankn
A rghts reserved. No part of ths book may be reproduced wthout the pror
wrtten permsson of the pubsher, except for persona use and the case of
bref quotatons embedded n artces or revews.
Every effort has been made to ensure the accuracy of the nformaton
presented. However, the nformaton contaned n ths book s sod wthout
warranty, ether express or mped. Nether the authors, Efend Pubshng
nor ts deaers or dstrbutors w be hed abe for any damages caused or
aeged to be caused drecty or ndrecty by ths book.
Efend Books has endeavored to provde trademark nformaton about a
companes and products mentoned n ths book, however we cannot
guarantee that ths nformaton s 100% accurate.
Frst Pubshed: 2012
Ths Edton Pubshed: 2012
Pubshed by Efendi Books
!234" 5- +,&$"&$#
Acknowedgments.............................................................................
Introducton......................................................................................
Chapter One..................................................................................... 1
Chapter Two................................................................................... 15
Chapter Three................................................................................ 37
Chapter Four .................................................................................. 46
Summary ....................................................................................... 52
6/7&,)4"8'"9"&$#
The frst person who needs thankng s |ame Rumbeow, who took what I
wrote and crafted t wth care nto ths MnBook youre readng rght now.
Im fary practsed at wrtng tutoras but brngng together a arge amount
of them nto a book s somethng I havent got the frst dea about. From
offerng to pubsh my book through Efend and puttng up wth my
questons to edtng content Id ony got to hm at the ast mnute, cheers.
Next beers on me when youre n London.
Of course I coudnt wrte a book on CoffeeScrpt wthout mentonng |eremy
Ashkenas, the creator and mantaner of CoffeeScrpt. Thanks for makng
wrtng |avaScrpt even more awesome.
Toby Howarth aso deserves a huge doft of my magnary cap. He s the
person who agreed to desgn the |avaScrpt Payground, the bog that I
aunched whch n my opnon got me ths far. Yet another person I seem to
owe a beer.
Id aso ke to thank a whoe craft of deveopers who got me to where I am
today wth ther bog posts, conference taks, tutoras and tweetng. Theres
so many that Im bound to have mssed a few but the names mmedatey
sprngng to mnd ncude Pau Irsh, Addy Osman, Rob Hawkes, Mchae
Heap, Dan Weman, Ph Sturgeon, E|ah Manor, Peter Cooper, Pau Leader
and a whoe oad of others too. Everythng these foks wrte I read and
consume regousy and I cant magne |ust how much Ive earned from
them.
Lasty Id ke to rase my gass to Rchard Ouck, who made a huge effort to
et me attend my frst web desgn conference back when I was fourteen. I
ceary remember comng home after that conference and reasng I wanted
to be nvoved n the web word for a ong tme to come. I often wonder
where Id be f I hadnt made t to that conference, and wthout Rch I dont
thnk I woud have.

:&$0,8;/$%,&
Over recent years, the popuarty of Test Drven Deveopment (TDD) has
surged. It has become an attractve choce for deveopers ookng to wrte
more robust and safer code. The TDD methadoogy has been decorated wth
mutpe spn-offs: Interacton-based Testng, Acceptance-test Drven
Deveopment, and most sgnfcanty, Behavour Drven Deveopment (BDD).
Devsed by Dan North, BDD s a rethnk of TDD, focusng on the users
behavour rather than that of each ndvdua unt of code.
There are penty of BDD frameworks for amost every modern anguage, but
for |avaScrpt, by far the most popuar choce s |asmne. |asmne can run n
a number of envronments, ncudng as a Ruby gem, for Node.|s, or as a
standaone test brary. Its concse syntax, expressve nature and smpe
setup has made t a ht wth deveopers.
In ths mnbook, I show you how to setup |asmne as a standaone brary,
ntegrate t wth CoffeeScrpt and bud a fuy functona CoffeeScrpt app
wth Behavour Drven Deveopment. I aso demonstrate the workfow I use
when practcng BDD that you may aso ke to adopt.
6##;9"8 <&,)4"8'"
Ths book assumes a workng knowedge of CoffeeScrpt, as we as the
bascs of Behavour/Test Drven Deveopment. A sma amount of experence
wth |asmne s a bonus but not requred. If youve not used CoffeeScrpt
before, a good pace to start woud be the CoffeeScrpt Documentaton,
whch s a fantastc prmer to the anguage. You mght fnd t hepfu to have
a copy of the |asmne docs open whe you read ths book.

In Chapter One we take a ook at downoadng and settng up |asmne, our testng
framework of choce. We examne the prncpes of TDD/BDD and ntroduce the app we be
budng, a basket system for an ecommerce store. We begn to wrte some tests for the
core basket functonaty and ook at how to best mpement the features we desgn.
Throughout ths book we're gong to bud a sma appcaton - whch we'
deveop through BDD - wrtng our tests frst, foowed by the code to make
those tests pass successfuy. The appcaton we' be budng s
deberatey basc so we can concentrate on the tests. It's |ust a smpe
shoppng basket; the type you'd see on any typca ecommerce store. We're
gong to wrte the code to manage ths basket: addng, edtng, deetng
tems, and then the checkout process, cacuatng the tota cost, appyng
dscounts and so on.
Every one of these features w have tests wrtten before we wrte the code.
The beauty of dong ths, as I' demonstrate, s that by wrtng tests frst,
and then watchng them fa, you get a step by step gude on how to
mpement the feature. In ths sense, the tests w not ony mprove the
quaty of the code, but they' aso make you consder the desgn more.
Everytme you run a test, you' get an error message. You can then attend
to that error, fx t, rerun the tests and see a dfferent error. Contnue n ths
fashon unt your functon s mpemented.
Tests are also often referred to as specs", particularly in the Ruby on Rails
community. ln this book l'll also refer to a test as a spec", and our tests as
a whole as specs".
Before we start any of that, et's do a bt of pannng. Our app w have two
man ob|ects: a basket and an tem. We' make these nto casses, and our
Basket w have Item ob|ects added to t.
The frst thng to do s get our fes set up. Downoad |asmne as the
Standaone Verson. The foder you downoad w have two drectores,
example and lib. I set mne structure up a bt dfferenty:
app/
dist/
2
I don't ke to have .coffee and .js fes n the same foder, so I separate
them out. Ths foder structure s by no means fxed; you shoud use a
structure that suts you.
Before we go onto wrtng some tests, I want to brefy tak about my
process for compng CoffeeScrpt fes. Athough some brares support
drecty testng CoffeeScrpt fes, |asmne doesn't, and expects |avaScrpt
fes. The app of choce I use for compaton s LveReoad. It's currenty
stabe on Mac and n apha on Wndows, so athough t's not qute there on
Wndows yet t s comng. Ths tte app s awesome, t sts hdden away and
automatcay compes CoffeeScrpt fes nto ther |S counterparts
automatcay (t aso supports many other fe types). Aong wth that, t w
aso automatcay refresh your browser wndow, meanng you can save your
*.coffee fe, and t's mmedatey comped and your tests run. Perfect. If
you'd rather stck to the command ne, the CoffeeScrpt documentaton has
spec/
{ compiled *.coffee spec files }
src
{ compiled *.coffee src files }
lib/
jasmine-1.1.0/
{ jasmine src files here }
spec/
BasketSpec.coffee
{ other spec files }
SpecRunner.html
src/
Basket.coffee
Item.coffee
{ other src files }
3
some exceent nformaton on compng. Take a ook at the watch
command to get thngs compng on save.
One other thng to note s that by defaut, CoffeeScrpt w wrap an
anonymous functon around the comped |avaScrpt:
However, what ths does s mean that none of your varabes are exposed
gobay. Ths s generay a good thng, but for us we need to make sure any
casses we do wrte (we' be wrtng two, Basket and Item) are exposed
gobay, so we can use them n other fes, and our test fes n partcuar.
There's two ways to go about ths. We can ether expcty expose the
casses, by creatng a new property on the goba window ob|ect:
Or, when compng your CoffeeScrpt, you need to te t not to wrap the
anonymous functon around your code. If you're compng n the termna,
|ust add --bare, or -b on the end of your command. Most vsua compers,
ncudng the aforementoned LveReoad, contan an opton for you to
enabe bare compng. Ths s the opton I've chosen to taken n ths book,
prmary to avod havng to have that extra ne at the end n exampes, but
fee free to use the frst method f you rather. I prefer t n a ot of stuatons,
especay f you are defnng a ot of varabes and want very strct contro
(function() {
//your compiled CS code here
}).call(this);
<h2 id="define_our_class_here">define our class here</h2>
<h2 id="right_at_the_bottom">right at the bottom:</h2>
window.Basket = Basket
4
over what s exposed gobay, but n ths case that's not so much of an
ssue.
Now we need to head to SpecRunner.html. Load t up n your favourte text
edtor, and make sure you're ncudng the new spec fes and our source
fes, too. The order s mportant, your src fes need to be ncuded before
the specs.
These shoud be ncuded after the script tags that ncude the |asmne
source fes. You can cose SpecRunner.html now, we wont be edtng t
agan. We' vst that fe n our browser - t's the fe that runs the specs -
but we don't need to edt t agan for a whe.
Now we've got that set up, et's frst pan out some nta tests. Load up
spec/BasketSpec.coffee. The dea behnd havng spec fes s that every
cass n your appcaton shoud have ts own set of specs, and that runnng
them shoud verfy that everythng s workng as expected. Each spec fe
shoud foow a specfc pattern:
<script type="text/javascript" src="dist/src/
Basket.js"></script>
<script type="text/javascript" src="dist/src/
Item.js"></script>
<script type="text/javascript" src="dist/spec/
BasketSpec.js"></script>
describe("Basket Class", function() {
// our individual tests go here
});
5
But of course, ths s testng wth CoffeeScript, so we can do:
Insde ths describe bock, we defne ndvdua tests ke so:
These are nested, so you get somethng ke:
Not ony can we nest it bocks, describe bocks can be nested n each
other. Ths gves us the abty to structure our tests much more:
describe "Basket Class", ->
# individual tests go here
it "should be able to add item to basket", ->
# code for testing this here
describe "Basket Class", ->
it "should be able to add item to basket", ->
# testing code
describe "Basket Class", ->
describe "Test Set 1", ->
# tests here
describe "Test Set 2", ->
# more tests here
6
Some peope choose to have |ust one spec fe for ther entre appcaton
and use describe bocks to organse tests wthn that one fe. Personay,
I'm a fan of keepng thngs organsed wthn mutpe fes. The eager-eyed
amongst you w rease I haven't made a spec fe for my Item cass. We're
eavng that for now, and concentratng on some basc specs for our Basket
cass.
Most tests w need some sampe data to work wth. You mght have heard
of these as fixtures n other testng frameworks. I tend to store my fxtures
n a |avascrpt ob|ect, whch I usuay ca test.
|asmne provdes us wth beforeEach; a method nsde our describe bock
whch s run before every test. We can use ths to set up our test data:
I've created a new Basket, and a sampe Item (one I wsh I owned!) that we
can add to the basket. A arge ma|orty of our tests w rey on the basket
havng an tem n. Rather than repcatng code, we can add that nto our
beforeEach:
test = {}
beforeEach ->
test.basket = new Basket()
test.item = new Item 001, "Macbook Air", "Newer,
thinner, better", 799
beforeEach ->
test.basket = new Basket()
test.item = new Item 001, "MacBook Air", "Newer,
thinner, better", 799
7
Of course, our Basket cass doesn't have an add method yet, but we sha
get to that shorty. Let's thnk what some of the basc functonaty w be,
then wrte our tests for t, and then the code to fx t. Rght now, we want to
be abe to:
Add an tem
Remove an tem
Cacuate the tota prce of a the tems
Let's tacke the add() method. Ths method w take two parameters: an
Item ob|ect and the quantty. The add() method shoud be smart enough so
f we add an tem that aready s n the basket, t w update the quantty.
Now we have a rough dea of what t shoud do, et's wrte tests for t:
item2 = new Item 002, "Magic Trackpad", "Better than a
mouse", 50
test.basket.add item2, 1
it "should be able to add a new item to basket", ->
priorCountVal = test.basket.distinctCount
test.basket.add test.item, 1
expect(test.basket.distinctCount).toEqual priorCountVal + 1
it "should be able to update quantity when adding an item
already in the basket", ->
priorCountVal = test.basket.getQuantity(001)
test.basket.add(test.item, 1)
expect(test.basket.getQuantity(001)).toEqual priorCountVal
+ 1
8
Tackng the frst test, here's how t works:
1. Get the current amount of tems n the basket (the number of
dfferent tems, not the tota amount of tems)
2. Add our test tem
3. Expect the dstnct count to equa the prevous vaue, pus 1
The second test works much the same:
1. Get the quantty of tems n the basket wth an d of 001 (our
MacBook Ar, athough t can be any arbtrary Item).
2. Add one more of that same tem to the basket.
3. Expect the new quantty to equa the prevous vaue, pus 1.
Here we meet |asmne's frst matcher, toEqual. We ca expect() - passng
t a vaue - and then use toEqual to compare t. There are many more types
of checks (or assertons) we can perform.
Now for the exctng bt. If we oad up SpecRunner.html n the browser, our
tests w run and we shoud get some faures. That's to be expected - we
haven't wrtten any code yet! You shoud have two faures: one for each
spec we wrote. Both shoud say:
Ths s an easy error; we've not created our Basket cass yet. Let's fx t.
Head over to src/Basket.coffee and add |ust one ne:
Now, we can rerun the tests and get a dfferent error:
ReferenceError: Basket is not defined
class Basket
9
Load up Item.coffee and add the one ne agan:
Remember, the process of BDD s to |ust fx the error you're gven, not to
wrte code to fx everythng at once. By budng up our codebase step by
step we can ensure that the whoe thng performs as we expect t to.
Runnng t agan, we get a new error:
Let's wrte our add() method.
We' need an array to store a our tems and then the add method w add
them to the array. Remember, we're not tryng to wrte the whoe
appcaton. We're ony tryng to fx the error we've |ust got. My Basket cass
now ooks ke ths:
ReferenceError: Item is not defined
class Item
TypeError: Object #<Basket> has no method 'add'
class Basket
items: []
distinctCount: 0
totalCount: 0
add: (item, quantity) ->
10
Ths s by no means a perfect mpementaton, however, ths s the beauty of
havng tests. Our tests w hghght the faws as we progress wth the
deveopment of our appcaton.
Refresh the SpecRunner.html fe and you shoud see that the frst spec
passes!
However, |ust because t passes, t doesn't mean that t's compete. As we
contnue on deveopng the appcaton, we' see ssues turn up and we'
need to refactor our tests and mpementaton code. Deveopng wth tests
s fantastc for ths reason; we can easy refactor wthout concern that we're
breakng anythng.
So, et's take a ook at what fas n our second spec:
We now need to take a step back and thnk about our mpementaton. In
our items array, how are we gong to store the tems? Is t worth storng the
entre ob|ect? What about the quantty?
It makes sense to store n the tem d and quantty; we' ony need to
reference tems. So, et's edt the add() method to refect ths:
@items.push item
@distinctCount++
@totalCount++
TypeError: Object #<Basket> has no method 'getQuantity'
11
Before we rerun the tests, now we after referencng item.id, and so far a
we've defned for our Item cass s:
It has no dea rght now what item.id s. To do ths we smpy need to
mpement the constructor method for our Item cass:
Here's a handy CoffeeScrpt shortcut: f you're usng the constructor method
|ust to set propertes, rather than do:
You can smpy shorten t to:
add: (item, quantity) ->
@items.push({
"item_id" : item.id,
"quantity": quantity
})
@distinctCount++
@totalCount++
class Item
constructor: (@id, @title, @desc, @cost) ->
constructor: (id) ->
@id = id
12
Now we're sayng that when we create a new Item, we're passng n four
parameters whch are set as the ID, tte, descrpton and cost. Ths now
makes sure when we use item.id, t knows what we're referencng.
We' aso mpement the getQuantity method:
Re-run the tests:
Ths s happenng because rght now, we aren't updatng the quantty f the
tem s aready n the basket. Let's fx that. Ths w requre a tte more
code:
constructor: (@id) ->
getQuantity: (item_id) ->
for i in @items
return i.quantity if i.item_id is item_id
false
Expected 1 to equal 2.
add: (item, quantity) ->
if @itemExistsInBasket(item.id)
curItemLoc = @getItemLocation item.id
@items[curItemLoc].quantity += quantity
else
@items.push({
13
Refresh our tests and you' see that they a pass.
I hope you're begnnng to see the way deveopers get hooked on
deveopng wth tests. You' notce we haven't created any form of front end
for our system yet. We're not beng dstracted by the GUI, by aesthetcs or
other unmportant mnutae. Thanks to our tests, we can put that to one
sde and concentrate soey on our appcaton's ogc.
In chapter two we' contnue wth more testng and ook at refactorng more
of our code.
"item_id" : item.id,
"quantity": quantity
})
@distinctCount++
@totalCount += quantity
itemExistsInBasket: (item_id) ->
for i in @items
return true if i.item_id is item_id
false
getItemLocation: (item_id) ->
count = 0
for i in @items
return count if i.item_id is item_id
count++
false
14
In Chapter Two we examne refactorng our exstng mpementaton and how tests come n
ncredby usefu. We abstract away some functonaty nto heper methods, wrte tests for
those methods and take a ook at a few new matchers n |asmne. We thnk through how the
man part of our appcaton works and how best to mpement t.
Now we've got our frst two tests done, and earned the bascs of |asmne,
t's tme to proceed and wrte some more. Once we've got to a natura
stoppng pont, we' take a ook at the code we have wrtten and refactor t,
usng our tests to confrm a successfu refactorng.
The frst ssue s that n our nta mpementaton, we added a getQuantity
method on the Basket cass:
However, we ddn't drecty test ths method. We have tested t ndrecty
through the test we had wrtten, but nothng tests ths method soey.
Where possbe, t's usuay best to wrte fu tests for a methods ntroduced
nto the code.
Sometmes I' spot the need to abstract functonaty nto a heper method
and wrte tests before code, whch s how I prefer to work, but often I end up
dong the reverse. In an dea word, the entre appcaton w be but by
spottng potenta heper methods and wrtng tests before mpementng.
I ke to add a the heper method tests n ther own describe bock and
then nest them n there. So my tests for getQuantity go under the
describe bock for my Basket cass ke so:
getQuantity: (item_id) ->
for i in @items
return i.quantity if i.item_id is item_id
false
describe "helper functions in the Basket class", ->
describe "getQuantity", ->
it "should return false if passed an id that is not in
16
I'm not dong anythng ground breakng here, |ust basc tests that verfy the
functonaty s as expected. In order, these tests do as foows:
1. Check that f an ID s passed that doesn't exst n the Basket, t
returns fase. We use |asmne's toBeFalsy() matcher to do ths.
Note that toBeFalsy means anythng that evaluates to be false, not
that s booean false. Ths ncudes undefined and 0 too. If you
wanted to check for |ust booean false you coud use toBe(false).
2. Smary to the frst test, we check that t works wth an ncorrect
nput.
3. Fnay, check a vad resut s returned for a vad ID
We aso added other heper methods n Chapter 1, namey
itemExistsInBasket and getItemLocation:
array", ->
expect(test.basket.getQuantity(12345)).toBeFalsy()
it "should return false if passed an invalid argument,
such as a string", ->
expect(test.basket.getQuantity("hello!")).toBeFalsy()
it "should return the quantity if given a valid id", ->
expect(test.basket.getQuantity(2)).toEqual 1
itemExistsInBasket: (item_id) ->
for i in @items
return true if i.item_id is item_id
false
getItemLocation: (item_id) ->
count = 0
17
So, once agan, I wrte out some basc tests to check them:
for i in @items
return count if i.item_id is item_id
count++
false
describe "itemExistsInBasket", ->
it "should return false if item id does not exist", ->
expect(test.basket.itemExistsInBasket(23455)).toBeFalsy()
it "should return true if item id does exist", ->
expect(test.basket.itemExistsInBasket(2)).toBeTruthy()
it "should return false if given an invalid argument,
such as a string", ->
expect(test.basket.itemExistsInBasket("hello")).toBeFalsy()
describe "getItemLocation", ->
it "should return the location of item when given valid
id", ->
expect(test.basket.getItemLocation(2)).toEqual 0
it "should return false if item doesn't exist", ->
expect(test.basket.getItemLocation(39)).toBeFalsy()
it "should return false if given a invalid input", ->
18
These tests foow a very famar pattern so I won't cover them n as much
deta as I dd for the getQuantity tests. The ony new matcher here s
toBeTruthy() whch, unsurprsngy, does the exact opposte of
toBeFalsy().
Now we have those covered, t's tme to wrte some new functonaty for
addng tems. The frst s that f we add an tem that aready exsts the tem
shoud not be dupcated. Instead, we shoud update the quantty of that
tem n the basket. To do ths we need to rework our add method and add a
coupe of propertes to the Basket cass.
Our tests are smpe:
Here, we're grabbng the count of the tem n the basket, adddng one more
of the same tem and expectng the vaue to have ncreased by one.
Let's now refactor the add method to support ths test:
expect(test.basket.getItemLocation("hello")).toBeFalsy()
it "should be able to update quantity when adding an item
already in the basket", ->
test.basket.add(test.item, 1)
priorCountVal = test.basket.getQuantity(1)
test.basket.add(test.item, 1)
expect(test.basket.getQuantity(1)).toEqual priorCountVal +
1
19
We're aso ntroducng a new property here, distinctCount. I fgure that
often we w need to cacuate ths vaue rather than cacuate t every tme;
keepng t as a runnng vaue w save us some computaton.
We're n a test-wrtng frame of mnd, so I'm gong to go ahead and wrte
some tests for that too:
add: (item, quantity) ->
if @itemExistsInBasket(item.id)
curItemLoc = @getItemLocation item.id
@items[curItemLoc].quantity += quantity
else
@items.push({
"item_id" : item.id,
"quantity": quantity,
"item" : item
})
@distinctCount++
@totalCount += quantity
it "should update the total count by 1 when adding a brand
new item", ->
priorCountVal = test.basket.totalCount
test.basket.add(test.item, 1);
expect(test.basket.totalCount).toEqual priorCountVal + 1
it "should increase total count by 1 when adding one more of
an item that already exists", ->
test.basket.add(test.item, 1)
priorCountVal = test.basket.totalCount
20
A of those tests are sef-expanatory. You mght thnk t's overk, but the
more tests we have coverng varous anges of our code, the better, surey?
Let's take a ook at cacuatng the tota costs. The three varants we need to
check here are:
1. Cacuatng the cost for a snge tem
2. Cacuatng the cost for a snge tem wth a quantty greater than 1.
3. Cacuatng the cost for mutpe tems wth quanttes greater than 1.
As you'd expect, the tests are very smpe (as you' fnd most tests are -
any compex tests often shoud be spt up):
test.basket.add(test.item, 1)
expect(test.basket.totalCount).toEqual priorCountVal + 1
it "should update distinct count when adding brand new
item", ->
priorCountVal = test.basket.distinctCount
test.basket.add(test.item, 1)
expect(test.basket.distinctCount).toEqual priorCountVal + 1
it "should not update distinct count when adding more of an
item that already exists", ->
test.basket.add(test.item, 1)
priorCountVal = test.basket.distinctCount
test.basket.add(test.item, 2)
expect(test.basket.distinctCount).toEqual priorCountVal
describe "calculating total cost", ->
it "should calculate the cost for a single item in the
basket", ->
21
The mpementaton s easy enough here. The frst error w be the od
favourte 'method cacuateTota() s undefned', and once you've fxed that
a we need to do s oop over everythng n the items array, cacuate the
cost, and keep a runnng tota:
Fnay, we need to tak about removng an tem. Ths s the ast bt of
functonaty we' add before stoppng and refactorng, but the bad news s
t's actuay pretty compex to do. Ths s because there's more than one
possbty to dea wth.
expect(test.basket.calculateTotal()).toEqual 50
it "should calculate the cost for 1 item type with
multiple quantities", ->
test.basket.add(test.item2, 3)
expect(test.basket.calculateTotal()).toEqual 200
it "should calculate cost for multiple items and multiple
quantities", ->
test.basket.add(test.item2, 2)
test.basket.add(test.item, 2)
expect(test.basket.calculateTotal()).toEqual 1750
calculateTotal: ->
total = 0
for i in @items
total += i.item.cost * i.quantity
total
22
What f the user wants to remove a certan quantty of ony one tem? What
f they want to removng one tem entrey from a basket?
Let's wrte a few tests:
describe "removing items", ->
it "should return false if item does not exist to remove",
->
expect(test.basket.remove(39)).toBeFalsy()
it "should remove a specific quantity if given a second
parameter", ->
test.basket.add(test.item, 5)
prevCountVal = test.basket.getQuantity(1);
test.basket.remove(1,1)
expect(test.basket.getQuantity(1)).toEqual 4
it "should remove all items if not given a second
parameter", ->
test.basket.add(test.item, 1)
test.basket.remove(1)
expect(test.basket.getQuantity(1)).toBeFalsy()
it "should remove all items if quantity given is more than
the quantity in basket", ->
test.basket.add(test.item, 2)
test.basket.remove(1, 5)
expect(test.basket.getQuantity(1)).toBeFalsy()
it "should remove all items if quantity given is the same
as the quantity in basket", ->
test.basket.add(test.item, 2)
23
There's a ot of tests there but they shoud make sense. Most tests ca the
remove() method and then use getQuantity() to check the correct
remova happened.
The way I've etched out remove() n my mnd s that t takes two
parameters: the tem ID and the quantty to remove. If a user wants to
remove one of an tem, they can |ust pass n an ob|ect ID and nothng ese.
The frst test, returnng fase f the tem does not exst, s easy to pass:
Note how I set a defaut vaue for quantity, f t's not passed n. We' use
that ater to fgure out what code to run. The code for removng a
quanttes of an tem s easy too. Here t s, on ts own - I' sot t nto our
mpementaton n a moment:
We can smpy grab the ocaton of the tem and ca .splice() to remove t
from our @items array.
test.basket.remove(1,2)
expect(test.basket.getQuantity(1)).toBeFalsy()
remove: (item_id, quantity="all") ->
return false if not @itemExistsInBasket item_id
i = @getItemLocation item_id
@items.splice(i,i+1)
24
The code for remvong a certan quantty of an tem s easy too - athough
ths doesn't ncude the check that the user s tryng to take away more
tems than exst. We' dea wth that n a second.
What I'd ke to do s package these up wthn methods, and then ca one of
them based on the parameters passed n. As these methods w not be used
outsde of the remove method, t makes sense |ust to decare them wthn t:
Note the use of the =>, whch mantans the scope of this (or @) wthn the
removeAll and removeQuantity functons. If I ddn't decare the functon
usng => and used ->, the vaue of this wthn those functons woud pont
to the remove method, not the Basket ob|ect.
Our next step s to smpy ca removeAll f the quantity passed nto
remove s set to "all" (whch s what t's set as defaut):
@items[item_loc].quantity -= quantity
remove: (item_id, quantity="all") ->
return false if not @itemExistsInBasket item_id
removeAll = (item_id) =>
i = @getItemLocation item_id
@items.splice(i,i+1)
removeQuantity = (quantity, item_loc) =>
@items[item_loc].quantity -= quantity
25
If t's not, then we have two possbtes:
1. If the quantty passed n s ess than the quantty n the basket,
remove the set quantty of an tem from the Basket.
2. If t's greater than or equa to the quantty n the basket, ca
removeAll.
In code form, that ooks ke so:
It ooks compex, but n reaty t's not. We can refactor ths code to make t
more readabe, whch we' do shorty.
To summarse, here's the remove method:
if quantity is "all"
removeAll item_id
else
loc = @getItemLocation item_id
item = @items[loc]
if item.quantity <= quantity
removeAll item_id
else
removeQuantity quantity, loc
remove: (item_id, quantity="all") ->
return false if not @itemExistsInBasket item_id
removeAll = (item_id) =>
i = @getItemLocation item_id
@items.splice(i,i+1)
26
So, we've wrtten a basc mpementaton, and at ths pont we've got 23
specs, a of whch pass. At a stage ke ths I ke to take a ook over what
I've wrtten so far, as you w neary aways be abe to spot bts of code that
you can mprove. Because we have a comprehensve set of specs, t's easy
to see f some refactorng we perform breaks somethng.
|ump nto basket.coffee and scan the fe, ookng out for paces where we
can tdy thngs up.
The frst thng I spot s on ne 12:
removeQuantity = (quantity, item_loc) =>
@items[item_loc].quantity -= quantity
if quantity is "all"
removeAll item_id
else
loc = @getItemLocation item_id
item = @items[loc]
if item.quantity <= quantity
removeAll item_id
else
removeQuantity quantity, loc
@items.push({
"item_id" : item.id,
"quantity": quantity,
"item" : item
})
27
It's ony a sma change, but et's remove the brackets, braces and commas,
and make our code ess |ava and more Coffee. We can deete them and
watch our tests pass:
A good start. I'm reay not over the moon wth how I mpemented the
remove method. If you take a ook t's pretty messy:
@items.push
"item_id" : item.id
"quantity": quantity
"item" : item
remove: (item_id, quantity="all") ->
return false if not @itemExistsInBasket item_id
removeAll = (item_id) =>
i = @getItemLocation item_id
@items[i] = null
@updateItems()
removeQuantity = (quantity, item_loc) =>
@items[item_loc].quantity -= quantity
if quantity is "all"
removeAll item_id
else
loc = @getItemLocation item_id
item = @items[loc]
if item.quantity <= quantity
removeAll item_id
28
I don't know about you, but that ooks pretty messy. Let's nvert the
condtona where we check for quantty. That's easy done:
I can do ths n fu confdence; a quck rerun of the tests show they st
pass. I'm aso not happy wth ths condtona:
To me, that's the knd of condtona I'd refactor nto a one ne ternary, but
CoffeeScrpt doesn't have an expct ternary operator. What we can do
however s put ths onto one ne, whch I thnk s fne n ths case:
else
removeQuantity quantity, loc
if quantity isnt "all"
loc = @getItemLocation item_id
item = @items[loc]
if item.quantity <= quantity
removeAll item_id
else
removeQuantity quantity, loc
else
removeAll item_id
if item.quantity <= quantity
removeAll item_id
else
removeQuantity quantity, loc
29
Agan, a rerun of the tests shows a passng set of specs. The abty to
refactor ke ths wth confdence s |ust great.
The fna pece of code I want to qucky refactor s:
In the case where I use if i isnt null I'd rather use CoffeeScrpt's
unless:
And agan the specs show 23 passes, so we're a set.
if item.quantity <= quantity then removeAll item_id else
removeQuantity quantity, loc
updateItems: ->
newArr = []
for i in @items
if i isnt null
newArr.push i
@items = newArr
updateItems: ->
newArr = []
for i in @items
unless i is null
newArr.push i
@items = newArr
30
Now et's move onto our Item cass, whch s ookng bare rght now as a
our tests have been based around the basket. A that Item.coffee contans
s:
I woud ke to wrte some methods to update an tem's propertes. Rather
than wrte ndvdua setters and getters, I am gong to wrte one functon
whch takes an ob|ect of propertes and vaues, and overwrtes the defned
ones. Frst though, et's wrte tests!
Create the fe app/spec/ItemSpec.coffee, and add ths ne to
SpecRunner.html:
Frst, I want to set up the testng bootstrap code, |ust ke before:
Now I can wrte the test for updatng an tem's propertes:
class Item
constructor: (@id, @title, @desc, @cost) ->
<script type="text/javascript" src="spec/
ItemSpec.js"></script>
describe "Item", ->
test = {}
beforeEach ->
test.item = new Item 1, "Magic Mouse", "Super awesome",
50
31
And of course, these tests fa. The frst error s the ack of an update
method, so et's fx that and then rerun the tests. Our error:
Ths error s happenng because the update method sn't actuay dong
anythng yet. If we then work on the mpementaton, we can fx t:
describe "updating an item", ->
it "should update only the properties passed to it", ->
test.item.update
"title": "The Magic Mouse"
"cost": 49.50
expect(test.item.title).toEqual "The Magic Mouse"
expect(test.item.cost).toEqual 49.50
expect(test.item.desc).toEqual "Super awesome"
it "should not be able to update the ID property", ->
test.item.update
"title": "The Magic Mouse"
"id": 49
expect(test.item.title).toEqual "The Magic Mouse"
expect(test.item.id).toEqual 1
Expected 'Magic Mouse' to equal 'The Magic Mouse'.
update: (opts) ->
for key of opts
if @[key]? and key isnt "id"
@[key] = opts[key]
32
Ths shoud be enough to get our tests passng.
If you take a ook at the mpementaton for update, you can see t prevents
the modfcaton of the id attrbute.
What f n the future we have more of these protected feds? It woud be
sy to keep addng to the condtona ke so:
So why don't we add n a way to add protected feds? Let's wrte some
tests:
if @[key]? and key isnt "id" and key isnt "foo" and key isnt
"bar"
describe "protected fields", ->
it "should be able to add a new protected field to the
array", ->
priorCount = test.item.protectedFields.length
test.item.addProtected "desc"
expect(test.item.protectedFields.length).toEqual
priorCount+1
it "should protect the ID field by default", ->
expect(test.item.protectedFields).toContain("id")
it "should stop the update method updating the field if
it's protected", ->
test.item.addProtected "desc"
test.item.update
33
After fxng the trva errors (defnng the method), we get ths error:
Notce above I've used a new |asmne matcher we haven't met yet,
toContain. You can pass t a strng or array and t returns true f that strng/
array contans the vaue you pass n.
Wthn the item constructor, add n @protectedFields = ["id"]. You
shoud see we now pass the mdde test, "t shoud protect the ID fed by
defaut" and we're gettng the error:
So t's tme to wrte the new code for addng a vaue to ths array:
Ths passes the frst test. The error now s:
"desc" : "new description"
expect(test.item.desc).toEqual "Super awesome"
Cannot read property 'length' of undefined
Item protected fields should be able to add a new protected
field to the array.
Expected 1 to equal 2.
addProtected: (field) ->
@protectedFields.push(field)
34
So, et's make our update method take ths array of feds nto account. To
do ths, I w create a new method whch tes us f a fed s protected. But
before we do, t's tme to wrte the tests!
The mpementaton s very easy:
Let's head back to our update method and fx the one fang test:
Item protected fields should stop the update method updating
the field if it's protected.
Expected 'new description' to equal 'Super awesome'.
describe "isProtected()", ->
it "should return true if field is protected", ->
expect(test.item.isProtected("id")).toBeTruthy()
it "should return false if field is not protected or does
not exist", ->
expect(test.item.isProtected("desc")).toBeFalsy()
expect(test.item.isProtected("foo")).toBeFalsy()
isProtected: (field) ->
for pF in @protectedFields
return true if field is pF
false
update: (opts) ->
for key of opts
35
And there you have t! 30 passng specs.
One fna thng before we move on. In an appcaton wth a arge sute of
specs, runnng them can get sow. Snce, at the moment, we're ony workng
on the Item cass, we reay ony want to see those specs. To sove ths,
|asmne aows us to run a subset of specs.
We can use |asmne's spec runner to fter by describe bock. In our
browser, f we append ?spec=Item, we w ony be shown any specs n that
bock.
if @[key]? and not @isProtected key
@[key] = opts[key]
36
In Chapter Three we revew and refactor our exstng code and extend our shoppng cart
further. We ntroduce the concept of dscounts and graduay bud up an ntegent
dscountng system. We aso ook at creatng our own |asmne matcher and extendng the
core system.
At ths pont, t's a good moment to revew where we're up to rght now. In
tota there's 30 passng specs whch te me the functonaty we have
mpemented.
The Basket:
Addng tems
Removng tems
Cacuatng tota cost
Varous heper functons for the above
.and the Item:
Updatng an tem
Protected feds on an tem
Functon to return f tem fed s protected or not
In ths chapter we w mpement more functonaty on both tems and
baskets. Currenty, there's a few basc features you'd expect to be n any
shoppng cart appcaton mssng and ths chapter w be about fng n the
hoes.
Of course, everythng we wrte w have tests wrtten for t frst and we'
deveop based off error messages from our tests. I' aso show you a coupe
more of |asmne's features.
The next thng I'd ke to take a ook at s mpementng dscounts. In reaty
when orderng onne you can often enter a dscount code that w gve you
a certan percentage off, or cheapest tem free, or some permutaton aong
those nes.
The nta mpementaton for us s gong to be very smpe. We can appy a
dscount on an entre basket whch w decrease the tota prce by a certan
percentage. The way I envsage dong ths s by havng a method on our
Basket ob|ect to appy the dscount.
38
We' need to check that t works propery and can't do thngs t shoudn't,
such as decreasng by more than 100% or ncreasng t, at a.
As you shoud be accustomed to by now, we're gong to wrte some tests.
Here's my frst test:
The way I thnk applyDiscount() w work s that you' pass n the amount
of percent you wsh to dscount by and then get the prce back. Ths prce
(for now) w not be saved anywhere so ths method shoud be easy enough
to mpement. I aso do two tests, |ust as a santy check.
I aso want to wrte tests to check we can't appy ncorrect dscounts:
describe "discounting the basket", ->
it "should correctly apply discounts", ->
expect(test.basket.applyDiscount(10)).toEqual 45
expect(test.basket.applyDiscount(50)).toEqual 25
describe "discounting the basket", ->
it "should correctly apply discounts", ->
expect(test.basket.applyDiscount(10)).toEqual 45
expect(test.basket.applyDiscount(50)).toEqual 25
it "should not be able to apply a discount more than
100%", ->
expect(test.basket.applyDiscount(120)).toEqual 0
it "should be able to deal with negative numbers and treat
them the same as positive numbers", ->
expect(test.basket.applyDiscount(-20)).toEqual 40
39
So, at ths stage we - unsurprsngy - have 3 fang tests. As aways, we
start wth the frst test, get that workng, and move onto the second, and so
on. The frst error s smpy "undefned s not a functon", so I can fx ths by
decarng the method.
Then we get the error "Expected undefned to equa 45.", so t's tme to
wrte the frst mpementaton. Remember, we ony need to pass the frst
fang test rght now, don't attempt to fx them a. Here's my nta
attempt:
I' |ust expan what I'm dong there - say we pass n 10 as the argument. I
dvde 10 by 100 (as t's a percentage). I then get the tota of the prce, and
then mutpy t by 1 subtracted from the percentage. Ths gves us a
mutper.
(1-10/100) works out as (1-0.1) whch means I mutpy the tota by 0.9,
reducng the t by 10%.
That does, as expected, pass the frst test, but we've now got:
Basket dscountng the basket shoud not be abe to appy a
dscount more than 100%. Expected -9.999999999999998 to
equa 0.
Turns out ths one s trva to pass:
applyDiscount: (amount) -> (@calculateTotal() *
(1-(amount/100)))
40
Smpy, f the amount s greater than one hundred, |ust mt t to one
hundred.
Fnay, our ast test fas:
It's gettng 60 as t's beng passed -20, eadng to t ncreasng the depost.
What we want s for negatve numbers to be treated the same as postve
numbers. I'm sure you've guessed the fx; |ust make use of Math.abs() to
get the absoute vaue:
And now we have a fuy passng set of specs once agan.
Next up, I'd ke to mprove the dscount functonaty by havng t save the
dscount. It seems to make sense that f I appy a dscount, the next tme I
ca calculateTotal(), t shoud have that dscount apped. As aways, ets
wrte the tests:
applyDiscount: (amount) ->
if amount > 100 then amount = 100
(@calculateTotal() * (1-(amount/100)))
Basket discounting the basket should be able to deal with
negative numbers and treat them the same as positive numbers.
Expected 60 to equal 40.
amount = Math.abs(amount)
41
And rght now we have the error:
Basket dscountng the basket shoud persst the dscount.
Expected 50 to equa 45.
For ths we need to make some changes:
Add n a discount property on the cass to store the dscount
Make calculateTotal() appy this.discount
Change applyDiscount() to set this.discount and then ca
calculateTotal()
So n the constructor for our Basket I can add n:
And then edt calculateTotal:
it "should persist the discount", ->
expect(test.basket.applyDiscount(10)).toBeDiscountedBy(10)
expect(test.basket.calculateTotal()).toEqual 45
@discount = 0
calculateTotal: ->
total = 0
for i in @items
total += i.item.cost * i.quantity
(total - ((@discount/100)) * total)
42
And fnay, applyDiscount:
A the changes are straght forward. The man thng here s how addng n
new features s done wth confdence that nothng ese has broken;
regresson bugs are a worry of the past when you've got a comprehensve
test sute.
What you mght notce here s that we have a ot of tests that are of a
smar structure. They nvove our applyDiscount method and then use
toEqual() to see the dscount worked. When you get a ot of tests foowng
a patten, t's often worth ookng f you can abstract them nto a custom
matcher. I aso ke dong ths for carty, at a quck gance the foowng:
Is cearer than:
The frst shows we're testng dscounts; the second doesn't. So, we w take
our dscount tests and wrte a matcher. Wrtng a custom matcher n
|asmne s easy, t's done n the beforeEach ca of a descrbe bock. We can
applyDiscount: (amount) ->
amount = Math.abs(amount)
if amount > 100 then amount = 100
@discount = amount
@calculateTotal()
expect(test.basket.applyDiscount(10)).toBeDiscountedBy(10)
expect(test.basket.applyDiscount(10)).toEqual(45)
43
aso customse the error message that s dspayed f a spec fas.
Unfortunatey wth |asmne there's no way to get at any vaue or ob|ect
other than what the method ca wthn expect() returned. Hence, our
toBeDiscounted method has to take two parameters: the orgna prce, and
the dscount to appy. The mpementaton ooks ke ths:
You can see n the beforeEach I ca @addMatchers, and then pass t an
ob|ect wth our new method. Ths takes two arguments, and then returns f
actual - whch s a varabe I create to store @actual, s equa to the
percentage cacuaton formua.
@actual aows us to get at the returned vaue from wthn the expect()
ca. I then set @message, whch s the message shown f the asserton fas.
Ths aows me to repace:
Wth:
beforeEach ->
@addMatchers
toBeDiscounted: (orig,discount) ->
actual = @actual
@message = -> "Expected #{actual} to be #{discount}%
of #{orig}"
actual is (orig * (1-(discount/100)))
expect(test.basket.applyDiscount(10)).toEqual 45
expect(test.basket.applyDiscount(10)).toBeDiscounted(50, 10)
44
Athough t's a tte onger, t's now much cearer what we're testng, t's
more DRY and t's much ncer to read.
45
In Chapter Four we take a ook at ntegratng a thrd party API and how best to test t. We
examne Spes, |asmnes stubbng framework, and take a ook at the best way to mnmse
dependences n our test sute.
Let's magne for a mnute that as part of our appcaton we need to make a
ca to some 3rd party servce to pu n data. Our appcaton can make an
A|AX request and get back some |SON to work wth.How do we test ths?
Ideay, we'd rather not perform the actua request. Ths woud make the
tests sow and dependent on an nternet connecton. Instead, we can use a
technque caed stubbng.
|asmne ncudes a stubbng brary caed Spes. Wth Spes, we can capture
cas to a specfc functon n our cass, and run other code, rather than
runnng the actua code, efectvey repacng the method under the hood.
We' wrte our stubbng functon and make t return some |SON, smuatng
the behavor of the A|AX brary.
Now, the |SON we're gong to get s gong to be somethng ke ths:
{
"ratings" : [{
"rating" : 4,
"review" : "This is a really great product",
"source" : "Amazon"
},
{
"rating" : 1,
"review" : "I didn't really like it that much it
wasnt very good",
"source" : "PC World"
},
{
"rating" : 3,
"review" : "It's pretty average.",
47
We're workng wth a made-up API here, but the dea s to show how we can
use Spes to emuate ths functonaty wthout makng the actua HTTP
request.
Frsty, before we do the mockng, wrte the test:
Essentay, we're checkng the code can correcty parse the |SON response.
You' notce the getRatings() method, and at the moment both these tests
fa because that doesn't exst. Athough we're not gong to be propery
mpementng the method to pu the |SON n I shoud defne t. Once
defned, my error becomes:
TypeError: Cannot read property 'ength' of undefned
Now we can use Spes to emuate the |SON beng returned. We decare the
spy n the beforeEach:
"source" : "Ebay"
}]
}
describe "getting ratings from websites", ->
it "should return three latest ratings", ->
expect(test.item.getRatings().ratings.length).toEqual 3
it "should be able to parse an individual rating's score",
->
expect(test.item.getRatings().ratings[0].rating).toEqual
4
48
The work s done n the spyOn ca. We pass n the ob|ect to spy on, and the
method to spy on. Ths on ts own doesn't reay do much; a that woud do
on tsef woud aow us to check f the method has been caed. The reay
cever part s andCallFake. It h|acks the method and executes whatever we
pass through.
You can see here I'm |ust returnng our parsed |SON ob|ect. Wth that, we
have 2 more passng specs, brngng our tota up to 36.
The next thng I want to take a ook at s wrtng tests retrospectvey for
code that's aready been wrtten. Imagne you've taken over ths pro|ect
after a bref hatus, and one of your overeager coeagues has wrtten code
wthout tests.
Frst, gve hm a sap from me (|ust kddng, an ev gance w do) and then
we can ook at fttng tests to t. The code he's wrtten s for appyng
coupons to a basket that get 10% off. They are adamant they've tested t
thoroughy but you know that nothng can test t better than some |asmne
specs.
Here's hs code:
beforeEach ->
spyOn(test.item, 'getRatings').andCallFake ->
JSON.parse('{"ratings":[{"rating":4,"review":"This is a
really great
product","source":"Amazon"},{"rating":1,"review":"I didnt
really like it that much it wasnt very good","source":"PC
World"},{"rating":3,"review":"Its pretty
average.","source":"Ebay"}]}')
49
I'm gong to add our tests to ths wthn the describe bock for dscounts, as
the both are reated, and then we can use our custom matcher wthout
havng to move t up a eve of scope.
The frst tests are easy enough and pass too:
You mght thnk that we're done, rght?
applyCoupon: (code) ->
if code in @coupons.validCodes
@applyDiscount(10)
else
false
coupons:
validCodes: ["AA12", "BB34", "CC45"]
describe "discounts via coupon codes", ->
it "should apply a 10% discount for a valid coupon code",
->
expect(test.basket.applyCoupon("AA12")).toBeDiscounted(50,
10)
expect(test.basket.applyCoupon("BB34")).toBeDiscounted(50,
10)
expect(test.basket.calculateTotal()).toBeDiscounted(50,
10)
50
The tests pass but that's not enough. If you ony ever test the postve
outcome, you're not thoroughy testng the functonaty. These tests, on
ther own, ony te us that applyCoupon w aways pass.
We need counter-tests to check that t w not appy a dscount n certan
stuatons (namey when the coupon s nvad) and then, wth both sets of
tests together, we can be confdent n our mpementaton.
My counter tests ook ke:
Now, wth a 38 specs passng, we can be confdent n our co-worker's
efforts. The tests a pass, he dd a good |ob, and we can go and refactor or
change the functonaty wthout concern.
it "should not apply the discount for invalid codes and
return false", ->
expect(test.basket.applyCoupon("WRONG")).toEqual false
expect(test.basket.applyCoupon("CC12")).toEqual false
expect(test.basket.calculateTotal()).toEqual 50
51
For a so caed MnBook weve covered a ot of concepts! If youre new to |asmne or the BDD
approach you may be eft feeng sghty overwhemed. If thats you, Id hghy recommend
rereadng agan, perhaps expandng the app wrtten n ths book, addng new features, or
creatng a new sde pro|ect or |avaScrpt pro|ect to test yoursef. Whst I hope readng ths
book has heped you, theres no substtute for sttng down yoursef and wrtng some tests.
CoffeeScrpt aso reay shnes when wrtng much more compex appcatons. Athough our
app s basc, n paces CoffeeScrpt saved us a ot of code over a pan |avaScrpt
mpementaton. In partcuar creatng a cass, addng methods, and smpfed operators have
saved us messy vana |avaScrpt.
If you want to contnue wth CoffeeScrpt and BDD, my advce s twofod. Frsty, get wrtng.
Come up wth an dea for a |avaScrpt brary, a pugn, or anythng smar, and force yoursef
to wrte tests frst. When I frst began my adventures n BDD, t was hard to see the
advantages. Why wrte tests for such basc functonaty? Why wrte tests at a? Its |ust tme
wasted I coud be usng to mpement, rght? In hndsght though, I am so gad I stuck wth t
and now Im reapng the rewards t brngs, the very ones I have conveyed and naed home
mutpe tmes n ths book.
If you strugge wth any of the concepts n ths book, fee free to get n contact wth me on
Twtter (@|ack_Frankn) and I be happy to hep. I hope youve found ths book usefu, and
thanks for readng!
!" !"!#$% !#$%
&''( &''())
Smart, succinct books for web
developers
https://efendibooks.com

Das könnte Ihnen auch gefallen