Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TDD with AngularJS

165ade163b08a3d18f6e36458cbbea05?s=47 Mike Tierney
November 08, 2014

TDD with AngularJS

Talk given at NationJS 2014.

In this talk we will build a simple AngularJS application using the principles of Test Driven Development (TDD). While we will primarily focus on using Karma and Jasmine to create unit tests, we will also look at creating end-to-end (commonly referred to as E2E) integration tests using Protractor.

165ade163b08a3d18f6e36458cbbea05?s=128

Mike Tierney

November 08, 2014
Tweet

Transcript

  1. TDD with AngularJS Michael Tierney NationJS November 8, 2014

  2. About me Director of UI Engineering at ( ) Co

    - organizer of Refresh Seattle Find me on both and as @ miketierney Intridea http :// intridea .com GitHub Twitter
  3. Resources for Today’s talk: Code Samples : Slides : https

    :// github .com / miketierney / tdd - with - angular https :// speakerdeck .com / miketierney / tdd - with - angularjs
  4. Why TDD? Added stability Improves inter - developer communication Makes

    maintenance easier
  5. Added Stability Can make changes with relative confidence that they

    won ’ t break something without you knowing Encourages you to consider how you are implementing your code Encourages loosely coupled modules
  6. Improved Communication Tests function as both documentation and API definition

    Impacts of changes can be more readily spotted in git di ffs , code review , etc .
  7. Maintenance Because behavior is defined in the tests , understanding

    what the code does is easier for other contributors
  8. Today’s Tools Angular Karma ( unit tests ) Protractor (

    E 2E , or “ end to end ”) Jasmine PhantomJS Gulp ( not required , but helpful )
  9. Today’s project Build a ToDo app using TDD .

  10. Foundation Bootstrap the NPM and Bower files $ c d

    ~ / P r o j e c t s / t d d - w i t h - a n g u l a r j s $ e c h o " { } " > > p a c k a g e . j s o n $ e c h o ' { " n a m e " : " T D D w i t h A n g u l a r " } ' > > b o w e r . j s o n
  11. Install Packages $ n p m i n s t

    a l l - - s a v e - d e v k a r m a g u l p $ b o w e r i n s t a l l - - s a v e a n g u l a r a n g u l a r - m o c k s a n g u l a r - r e s o u r c e
  12. Con gure Karma $ n o d e _ m

    o d u l e s / k a r m a / b i n / k a r m a i n i t k a r m a . c o n f . j s … and just follow the on - screen instructions . Accept all of the defaults , and add both Chrome and PhantomJS as the browsers .
  13. Con gure Karma (cont’d)

  14. Con gure Karma (cont’d) Add the bower components to Karma

    : / / k a r m a . c o n f . j s / / l i s t o f f i l e s / p a t t e r n s t o l o a d i n t h e b r o w s e r f i l e s : [ ' b o w e r _ c o m p o n e n t s / a n g u l a r / a n g u l a r . m i n . j s ' , ' b o w e r _ c o m p o n e n t s / a n g u l a r - m o c k s / a n g u l a r - m o c k s . j s ' , ' b o w e r _ c o m p o n e n t s / a n g u l a r - r e s o u r c e / a n g u l a r - r e s o u r c e . j s ' , ' j s / * . j s ' , ' t e s t / * * / * _ s p e c . j s ' ] ,
  15. Con gure Gulp / / g u l p f

    i l e . j s v a r g u l p = r e q u i r e ( ' g u l p ' ) , k a r m a = r e q u i r e ( ' k a r m a ' ) . s e r v e r ; / * * * R u n t e s t o n c e a n d e x i t * / g u l p . t a s k ( ' t e s t ' , f u n c t i o n ( d o n e ) { k a r m a . s t a r t ( { c o n f i g F i l e : _ _ d i r n a m e + ' / k a r m a . c o n f . j s ' , s i n g l e R u n : t r u e } , d o n e ) ; } ) ;
  16. Create the directories $ m k d i r j

    s t e s t
  17. index.html < ! D O C T Y P E

    h t m l > < h t m l l a n g = " e n " > < h e a d > < m e t a c h a r s e t = " U T F - 8 " / > < t i t l e > T o D o T D D < / t i t l e > < / h e a d > < b o d y n g - a p p = " t o d o A p p " > < s c r i p t s r c = " / b o w e r _ c o m p o n e n t s / a n g u l a r / a n g u l a r . m i n . j s " > < / s c r i p t > < s c r i p t s r c = " / b o w e r _ c o m p o n e n t s / a n g u l a r - r e s o u r c e / a n g u l a r - r e s o u r c e . m i < s c r i p t s r c = " / j s / a p p . j s " > < / s c r i p t > < / b o d y > < / h t m l >
  18. js/app.js ' u s e s t r i c

    t ' ; a n g u l a r . m o d u l e ( ' t o d o A p p ' , [ ] ) ; Nothin ’ fancy yet ; just a bare minimum app .
  19. Application specs It should display a list of to do

    items It should allow a user to add new to do items It should allow a user to toggle the item state It should allow a user to delete an item It should store the To Do items on a remote server
  20. Let’s get going! Set up the rst test. / /

    t e s t / c o n t r o l l e r . j s ' u s e s t r i c t ' ; d e s c r i b e ( ' T o D o C o n t r o l l e r ' , f u n c t i o n ( ) { b e f o r e E a c h ( f u n c t i o n ( ) { v a r c o n t r o l l e r , s c o p e ; m o d u l e ( ' t o d o A p p . c o n t r o l l e r s ' ) ;
  21. …a little more setup… i n j e c t

    ( f u n c t i o n ( $ c o n t r o l l e r , $ r o o t S c o p e ) { c o n t r o l l e r = $ c o n t r o l l e r ; s c o p e = $ r o o t S c o p e . $ n e w ( ) ; } ) ; t h i s . c r e a t e C o n t r o l l e r = f u n c t i o n ( ) { r e t u r n c o n t r o l l e r ( ' T o D o A p p C o n t r o l l e r ' , { $ s c o p e : s c o p e } ) ; } ; t h i s . c t r l = t h i s . c r e a t e C o n t r o l l e r ( ) ; } ) ; / / e n d o f b e f o r e E a c h ( )
  22. Finally , a test! i t ( ' s h

    o u l d h a v e a t o d o s a r r a y ' , f u n c t i o n ( ) { e x p e c t ( t h i s . c t r l . t o d o I t e m s ) . t o E q u a l ( [ ] ) ; } ) ; } ) ; / / e n d o f D e s c r i b e ( ' T o D o C o n t r o l l e r ' )
  23. Jasmine Matchers e x p e c t ( a

    ) . t o B e ( b ) ; e x p e c t ( a ) . n o t . t o B e ( n u l l ) ; e x p e c t ( m e s s a g e ) . t o M a t c h ( / b a r / ) ; e x p e c t ( n u l l ) . t o B e N u l l ( ) ; e x p e c t ( a . f o o ) . t o b e D e f i n e d ( ) ; e x p e c t ( a . b a r ) . t o B e U n d e f i n e d ( ) ; e x p e c t ( f o o ) . t o B e T r u t h y ( ) ; e x p e c t ( a ) . t o B e F a l s y ( ) ; e x p e c t ( [ ' f o o ' , ' b a r ' , ' b a z ' ] ) . t o C o n t a i n ( ' b a r ' ) ; e x p e c t ( b a r ) . t o T h r o w ( ) ;
  24. Run the test $ g u l p t e

    s t
  25. Aaaaaand fail

  26. Create the controller / / j s / a p

    p . j s ' u s e s t r i c t ' ; a n g u l a r . m o d u l e ( ' t o d o A p p . c o n t r o l l e r s ' , [ ] ) . c o n t r o l l e r ( ' T o D o A p p C o n t r o l l e r ' , f u n c t i o n ( $ s c o p e ) { } ) ; a n g u l a r . m o d u l e ( ' t o d o A p p ' , [ ' t o d o A p p . c o n t r o l l e r s ' ] ) ;
  27. Still failing But failing di fferently !

  28. One more adjustment… a n g u l a r

    . m o d u l e ( ' t o d o A p p . c o n t r o l l e r s ' , [ ] ) . c o n t r o l l e r ( ' T o D o A p p C o n t r o l l e r ' , f u n c t i o n ( $ s c o p e ) { t h i s . t o d o I t e m s = [ ] ; } ) ;
  29. Look ma, all green!

  30. Add some functionality We need add and edit abilities .

    Let ’ s start with add .
  31. Create the test… The basic requirement of a d d

    I t e m is that it add a new i t e m to the t o d o I t e m s Array . / / i n t e s t / c o n t r o l l e r . j s , a f t e r t h e f i r s t " i t ( ) " s t a t e m e n t d e s c r i b e ( ' c t r l . a d d I t e m ' , f u n c t i o n ( ) { i t ( ' s h o u l d a d d a n e w i t e m t o t o d o I t e m s ' , f u n c t i o n ( ) { e x p e c t ( t h i s . c t r l . t o d o I t e m s . l e n g t h ) . t o B e ( 0 ) ; t h i s . c t r l . a d d I t e m ( { n a m e : ' N e w T o D o I t e m ' } ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s ) . t o E q u a l ( [ { n a m e : ' N e w T o D o I t e m ' } ] ) } ) ; } ) ;
  32. Add the new function… t h i s . a

    d d I t e m = f u n c t i o n ( n e w I t e m ) { t h i s . t o d o I t e m s . p u s h ( n e w I t e m ) ; } ; And we ’ re green again .
  33. Do the same for Edit… i t ( ' s

    h o u l d t o g g l e t h e i t e m s t a t e ' , f u n c t i o n ( ) { v a r t o g g l e C o m p l e t e = t h i s . c t r l . t o g g l e C o m p l e t e ; t h i s . c t r l . t o d o I t e m s . f o r E a c h ( f u n c t i o n ( i t e m ) { t o g g l e C o m p l e t e ( i t e m ) ; } ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s [ 0 ] . c o m p l e t e ) . t o B e T r u t h y ( ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s [ 1 ] . c o m p l e t e ) . t o B e F a l s y ( ) ; t h i s . c t r l . t o d o I t e m s . f o r E a c h ( f u n c t i o n ( i t e m ) { t o g g l e C o m p l e t e ( i t e m ) ; } ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s [ 0 ] . c o m p l e t e ) . t o B e F a l s y ( ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s [ 1 ] . c o m p l e t e ) . t o B e T r u t h y ( ) ; } ) ;
  34. Edit is more complicated, so check other cases as well

    i t ( ' s h o u l d c r e a t e a s t a t e i f n o n e e x i s t s ' , f u n c t i o n ( ) { v a r i t e m s = t h i s . c t r l . t o d o I t e m s ; t h i s . c t r l . t o d o I t e m s . p u s h ( { n a m e : ' N o c o m p l e t e p r o p e r t y ' } ) ; v a r i t e m = i t e m s [ i t e m s . l e n g t h - 1 ] ; e x p e c t ( i t e m . c o m p l e t e ) . t o B e U n d e f i n e d ( ) ; t h i s . c t r l . t o g g l e C o m p l e t e ( i t e m ) ; e x p e c t ( i t e m . c o m p l e t e ) . t o B e T r u t h y ( ) ; } ) ; / / . . . o t h e r t e s t s . . .
  35. t h i s . t o g g l

    e C o m p l e t e = f u n c t i o n ( i t e m ) { i t e m . c o m p l e t e = ! i t e m . c o m p l e t e ; } ;
  36. …and Delete d e s c r i b e

    ( ' c t r l . d e l e t e I t e m ' , f u n c t i o n ( ) { i t ( ' s h o u l d r e m o v e t h e i t e m ' , f u n c t i o n ( ) { v a r i = 0 , a d d I t e m = t h i s . c t r l . a d d I t e m , i t e m s = [ { n a m e : ' F i r s t I t e m ' } , { n a m e : ' S e c o n d I t e m ' } ] ; w h i l e ( i < = 3 ) { a d d I t e m ( i t e m s [ i ] ) ; i + + ; } t h i s . c t r l . d e l e t e I t e m ( 1 ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s ) . t o E q u a l ( [ i t e m s [ 0 ] ] ) ; } ) ; } ) ;
  37. t h i s . d e l e t

    e I t e m = f u n c t i o n ( i n d e x ) { c t r l . t o d o I t e m s . s p l i c e ( i n d e x , 1 ) ; } ;
  38. Building the Service Using ngResource , this is fairly straightforward

    .
  39. The test setup d e s c r i b

    e ( ' T o D o S e r v i c e ' , f u n c t i o n ( ) { b e f o r e E a c h ( f u n c t i o n ( ) { m o d u l e ( ' t o d o A p p . s e r v i c e s ' ) ; i n j e c t ( f u n c t i o n ( $ h t t p B a c k e n d , $ i n j e c t o r ) { t h i s . t o d o = $ i n j e c t o r . g e t ( ' T o D o ' ) ; t h i s . h t t p B a c k e n d = $ h t t p B a c k e n d ; } ) ; } ) ; } ) ;
  40. A couple of tests / / I n t h

    a t d e s c r i b e b l o c k i t ( ' s h o u l d h a v e C R U D c l a s s m e t h o d s ' , f u n c t i o n ( ) { e x p e c t ( t h i s . t o d o . g e t ) . t o B e D e f i n e d ( ) ; / / f o r i n d i v i d u a l i t e m e x p e c t ( t h i s . t o d o . q u e r y ) . t o B e D e f i n e d ( ) ; / / f o r a l l i t e m s e x p e c t ( t h i s . t o d o . s a v e ) . t o B e D e f i n e d ( ) ; / / c r e a t e } ) ; i t ( ' s h o u l d h a v e C R U D i n s t a n c e m e t h o d s ' , f u n c t i o n ( ) { v a r t o d o = n e w t h i s . t o d o ( ) ; e x p e c t ( t o d o . $ s a v e ) . t o B e D e f i n e d ( ) ; / / u p d a t e e x p e c t ( t o d o . $ d e l e t e ) . t o B e D e f i n e d ( ) ; / / d e l e t e e x p e c t ( t o d o . $ r e m o v e ) . t o B e D e f i n e d ( ) ; / / a l i a s f o r d e l e t e } ) ;
  41. Changes to the application a n g u l a

    r . m o d u l e ( ' t o d o A p p . s e r v i c e s ' , [ ' n g R e s o u r c e ' ] ) . f a c t o r y ( ' T o D o ' , f u n c t i o n ( $ r e s o u r c e ) { r e t u r n $ r e s o u r c e ( ' / t o d o s / a p i / : i d ' ) ; } ) ;
  42. Using $httpBackend Provided by the ngMock module ( filename is

    angular - mocks ) Makes it so you don ’ t need a backend to run tests ( a good thing ) Will watch for requests and return responses
  43. When do I need it? The most obvious is when

    you see errors that look like this : i t ( ' s h o u l d q u e r y f r o m / t o d o s / a p i ' , f u n c t i o n ( ) { t h i s . t o d o . q u e r y ( ) ; / / c a u s e d t h e e r r o r } ) ;
  44. $httpBackend test i t ( ' s h o u

    l d q u e r y f r o m / t o d o s / a p i ' , f u n c t i o n ( ) { t h i s . h t t p B a c k e n d . e x p e c t G E T ( ' / t o d o s / a p i ' ) . r e s p o n d ( [ ] ) ; / / m u s t r e t u r n * s o m e t h i n g * t h i s . t o d o . q u e r y ( ) ; t h i s . h t t p B a c k e n d . f l u s h ( ) ; } ) ;
  45. Integrate the service Time to hook everything together !

  46. Update the test con g / / i n t

    e s t / c o n t r o l l e r _ s p e c . j s i n j e c t ( f u n c t i o n ( $ c o n t r o l l e r , $ r o o t S c o p e , $ h t t p B a c k e n d , $ i n j e c t o r ) { / / . . . o t h e r i n j e c t i o n s t h i s . t o d o B a c k e n d = $ i n j e c t o r . g e t ( ' T o D o ' ) ; t h i s . h t t p B a c k e n d = $ h t t p B a c k e n d ; } ) ;
  47. Set up the test d e s c r i

    b e ( ' c t r l . a d d I t e m ' , f u n c t i o n ( ) { b e f o r e E a c h ( f u n c t i o n ( ) { t h i s . i t e m = { n a m e : ' N e w T o D o I t e m ' } ; s p y O n ( t h i s . t o d o B a c k e n d . p r o t o t y p e , ' $ s a v e ' ) . a n d . c a l l T h r o u g h ( ) ; t h i s . h t t p B a c k e n d . w h e n P O S T ( / t o d o s \ / a p i \ ? . * / ) . r e s p o n d ( t h i s . i t e m ) ; } ) ;
  48. Test the service call i t ( ' s h

    o u l d s a v e t o t h e T o D o s e r v i c e ' , f u n c t i o n ( ) { t h i s . c t r l . a d d I t e m ( { n a m e : ' N e w T o D o I t e m ' } ) ; e x p e c t ( t h i s . t o d o B a c k e n d . p r o t o t y p e . $ s a v e ) . t o H a v e B e e n C a l l e d W i t h ( t h i s . i t e m , j a s m i n e . a n y ( F u n c t i o n ) ) ; } ) ;
  49. Write the test i t ( ' s h o

    u l d s a v e t o t h e T o D o s e r v i c e ' , f u n c t i o n ( ) { e x p e c t ( t h i s . c t r l . t o d o I t e m s . l e n g t h ) . t o B e ( 0 ) ; t h i s . c t r l . a d d I t e m ( t h i s . i t e m ) ; t h i s . h t t p B a c k e n d . f l u s h ( ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s . l e n g t h ) . t o B e ( 1 ) ; e x p e c t ( t h i s . c t r l . t o d o I t e m s [ 0 ] . n a m e ) . t o B e ( t h i s . i t e m . n a m e ) ; } ) ; } ) ; / / e n d o f d e s c r i b e ( ' c t r l . a d d I t e m ' ) ;
  50. Update the controller / / A t t h e

    t o p o f t h e c o n t r o l l e r a n g u l a r . m o d u l e ( ' t o d o A p p . c o n t r o l l e r s ' , [ ' t o d o A p p . s e r v i c e s ' ] ) . c o n t r o l l e r ( ' T o D o A p p C o n t r o l l e r ' , f u n c t i o n ( $ s c o p e , T o D o ) { The controller is dependent on the services , so we tell Angular about that , then load load the service
  51. Update the method t h i s . a d

    d I t e m = f u n c t i o n ( n e w I t e m ) { v a r i t e m = n e w T o D o ( ) ; i t e m . $ s a v e ( n e w I t e m , f u n c t i o n ( ) { c t r l . t o d o I t e m s . p u s h ( i t e m ) ; } ) ; } ;
  52. E2E with Protractor

  53. Install Protractor $ n p m i n s t

    a l l - - s a v e - d e v p r o t r a c t o r $ n o d e _ m o d u l e s / p r o t r a c t o r / b i n / w e b d r i v e r - m a n a g e r u p d a t e $ m k d i r e 2 e
  54. Con gure Protractor Create an e 2e .conf .js file

    and add e x p o r t s . c o n f i g = { s e l e n i u m A d d r e s s : ' h t t p : / / l o c a l h o s t : 4 4 4 4 / w d / h u b ' , s p e c s : [ ' e 2 e / s p e c . j s ' ] } ;
  55. Add a test / / i n e 2 e

    / s p e c . j s d e s c r i b e ( ' h o m e p a g e ' , f u n c t i o n ( ) { i t ( ' s h o u l d h a v e a t i t l e ' , f u n c t i o n ( ) { b r o w s e r . g e t ( ' h t t p : / / l o c a l h o s t : 8 0 0 0 / i n d e x . h t m l ' ) ; e x p e c t ( b r o w s e r . g e t T i t l e ( ) ) . t o E q u a l ( ' T o D o T D D ' ) ; } ) ; } ) ;
  56. Run the servers In multiple terminal windows ( yeah ,

    it ’ s awkward ): $ n o d e _ m o d u l e s / p r o t r a c t o r / b i n / w e b d r i v e r - m a n a g e r s t a r t and # t h i s c a n b e r e p l a c e d b y a n y o t h e r s e r v e r , # b u t o u r a p p d o e s n ' t h a v e a w e b s e r v e r $ p y t h o n - m S i m p l e H T T P S e r v e r
  57. If you feel like this … Don ’ t worry

    , you ’ re not alone .
  58. Run the test ( nally) $ n o d e

    _ m o d u l e s / p r o t r a c t o r / b i n / p r o t r a c t o r e 2 e . c o n f . j s
  59. Protractor usage Uses the existing test framework Has some additional

    helper methods and global variables : b r o w s e r p r o t r a c t o r e l e m e n t b y Can do DOM traversal
  60. Resources by Evan Hahn Jasmine ’ s Documentation : Angular

    ’ s Tutorial : Protractor Tutorial : JavaScript T esting with Jasmine : JavaScript Behavior - Driven Development jasmine .github .io /2.0/ introduction .html https :// docs .angularjs .org / tutorial https :// github .com / angular / protractor / blob / master / docs / tutorial .md
  61. Thank You Gracias Danke Merci 谢谢 ありがとう