Testing AngularJS: inject, spies and $provide

Testing is an important part of software development. To a software developer, automating that testing is an important part of software development, that is because to a software developer (at least one like me) testing is boring…unless we can make it seem like software development.

In my last few posts on AngularJS we looked at a way to monitor HTTP activity and guard against page navigation when requests were pending. However, we didn't validate that the code actually worked. Fortunately, the great ways to encapsulate client-side business logic and isolate it from the user experience that AngularJS provides, coupled with excellent support from angular-mocks, make testing AngularJS easy. In this post, we will take a glimpse at how.

Jasmine, CoffeeScript and Chutzpah

There are a few approaches to JavaScript testing, but they usually involve the same general components; a test framework, a test runner, and a test language. Thankfully (for me and you), this post is not an exhaustive discussion of testing options or their pros and cons. Instead, I will be stating what I use and assuming that they are the best choice1. A great place to start is the JavaScript Testing Tactics presentation from Justin Searls, which can be found here along with other talks he has given.

Based on Justin's testing tactics, which I saw at SEMjs, I write all my tests using Jasmine, CoffeeScript and jasmine-given.  The outcome is a terse testing DSL2 that is low on ceremony and high on readability3.

Chutzpah

While it is common on greenfield projects to use a test runner such as Karma launched by Gulp or Grunt, I started my work on a legacy project where the build process was maintained using Visual Studio and MSBuild. Chutzpah is a suite of tools that fits this development process nicely, including a test runner as a NuGet package and some simple Visual Studio integration via extensions.

In addition, Chutzpah supports multiple testing frameworks4, multiple languages5 and code coverage metrics using Blanket.js. Basically, Chutzpah is awesome; fact.

Testing an Angular Factory

With our test framework, test runner and test language selected, we can look at our first test.  We are going to test saHttpActivityInterceptor and the very first thing we should test is that saHttpActivityInterceptor  actually exists. The following test does exactly that.

###
## <reference path="../angular-sa.js"/>
###

describe 'saHttpActivityInterceptor', ->
  Given -> module 'somewhatabstract'
  describe 'exists', ->
    When => inject (saHttpActivityInterceptor) =>
      @saHttpActivityInterceptor = saHttpActivityInterceptor
    Then => expect(@saHttpActivityInterceptor).toBeDefined()

Just as with AngularJS implementations, AngularJS tests start with some setup: a reference path to the file under test (other files such as the Jasmine framework, AngularJS and angular-mocks are included via the Chutzpah configuration file), a  describe call under which to group all tests for  saHttpActivityInterceptor, and a Given call that ensures the somewhatabstract module is loaded at the start of each child test (the module method is provided by Angular Mocks).

The actual unit test is declared starting at line 7 with describe 'exists', ->. This test is very simple; it states that when we try to inject our factory, we should get something other than undefined. The => syntax in CoffeeScript (also known as "fat arrow" syntax6) ensures that the When and Then calls share the same this context so that the @saHttpActivityInterceptor variable is shared between them (the @ symbol preceeding a variable in CoffeeScript indicates a context-level variable). The value stored in the @saHttpActivityInterceptor variable is obtained by asking AngularJS to inject it using the inject function, a helpful utility from angular-mocks.

This test works, you can verify it easily by commenting out the interceptor declaration in the JavaScript file we referenced7, but it is not a great test. If saNavigationGuard  does not exist, this test will fail, yet our interceptor still exists. What we have done is create a simple integration test instead of a unit test; we need to isolate the thing under test, saHttpActivityInterceptor, from its dependency, saNavigationGuard.

inject and $provide

To isolate our item under test, we need to provide our own version of saNavigationGuard . We can do this using a fake; an object that pretends to be the real thing. We will use a Jasmine spy as a fake to represent saNavigationGuard and then provide it to AngularJS using the $provide  service8. Because AngularJS uses the most recent definition when injecting dependencies and because our newly created spy is the most recent definition of saNavigationGuard, it is that spy which ultimately gets injected into saHttpActivityInterceptor when the test runs.

###
## <reference path="../angular-sa.js"/>
###

describe 'saHttpActivityInterceptor', ->
  Given -> module 'somewhatabstract'
  Given -> module ($provide) ->
    fakeNavigationGuard = jasmine.createSpyObj 'saNavigationGuard', ['registerGuardian']
    $provide.value 'saNavigationGuard', fakeNavigationGuard;return
      
  describe 'exists', ->
    When => inject (saHttpActivityInterceptor) =>
      @saHttpActivityInterceptor = saHttpActivityInterceptor
    Then => expect(@saHttpActivityInterceptor).toBeDefined()

Not only are we now isolating saHttpActivityInterceptor , but because our fake is being used in place of the real saNavigationGuard, we can check that any guardian registered with our fake works properly9.

###
## <reference path="../angular-sa.js"/>
###

describe 'saHttpActivityInterceptor', ->
  Given -> module 'somewhatabstract'
  Given -> module ($provide) ->
    fakeNavigationGuard = jasmine.createSpyObj 'saNavigationGuard', ['registerGuardian']
    $provide.value 'saNavigationGuard', fakeNavigationGuard;return
      
  describe 'exists', ->
    When => inject (saHttpActivityInterceptor) =>
      @saHttpActivityInterceptor = saHttpActivityInterceptor
    Then => expect(@saHttpActivityInterceptor).toBeDefined()
      
  describe '#guardian', ->
    describe 'is registered', ->
      Given => inject (saNavigationGuard) =>
        @saNavigationGuard = saNavigationGuard
      When => inject (saHttpActivityInterceptor) =>
      Then => expect(@saNavigationGuard.registerGuardian).toHaveBeenCalledWith jasmine.any(Function)
      
    describe 'returns undefined when there are no pending requests', ->
      Given => inject (saNavigationGuard) =>
        saNavigationGuard.registerGuardian.and.callFake (guardian) => @guardian = guardian
        inject (saHttpActivityInterceptor) =>
      When => @actual = @guardian()
      Then => expect(@actual).toBeUndefined()
    
    describe 'returns message when there are pending requests', ->
      Given => inject (saNavigationGuard) =>
        saNavigationGuard.registerGuardian.and.callFake (guardian) => @guardian = guardian
        inject (saHttpActivityInterceptor) => saHttpActivityInterceptor.request { method: "POST" }
      When => @actual = @guardian()
      Then => expect(@actual).toBeDefined()

And there we have it, a little test suite that validates the saHttpActivityInterceptor, almost. You may note that to test the guardian, we had to actually use one of the factory functions, request, but how do we know that the request function works if we haven't tested it? We should add some tests, but since we can only check the functionality of requestresponse, and responseError via the guardian call and we can only check the guardian call via the functionality of those other methods, we don't have a good way to gain high confidence in the functionality of any of these methods. Therefore, if we really wanted to test this effectively, we need to refactor the count functionality into its own factory. That way we can inject and validate the count state independently of the thing under test. For now, that's an exercise for another time.

Finally…

In this post, I have shown how we can test a simple AngularJS factory using Jasmine, Jasmine-Given and the built-in testing support of the AngularJS framework via Angular Mocks; specifically, module,  inject and $provide.

While not exhaustive, I hope this look at testing Angular-based code encourages you to begin testing your own applications. Code discussed in this and earlier related posts can be found in a repository on GitHub.  The repository includes a Visual Studio solution and project to run the tests, including NuGet and Bower restore to get the appropriate packages for running the tests. I intend to expand the code in GitHub as I write more blogs on AngularJS and AngularJS testing.

In the next post, we will take a look at how I structure directives to simplify testing and what that testing looks like. We might even see some of the cooler testing tricks for AngularJS that enable us to synchronously test asynchronous operations and validate web requests. Until then, feel free to ask questions in the comments and carry on coding.

  1. I recommend that you investigate for yourself before choosing what works best for you and your development processes []
  2. Domain-specific Language []
  3. IMHO YMMV []
  4. Jasmine, QUnit and Mocha []
  5. CoffeeScript, TypeScript or plain old JavaScript []
  6. Be careful when using this "fat arrow" syntax – sharing context across unit tests can cause side-effects including false pass and fail results []
  7. Go on, try it. The code is on GitHub []
  8. Make sure to use $provide before any calls to inject []
  9. Though we could also have done this by spying on the real saNavigationGuard.registerGuardian function using Jasmine's spyOn function, such an approach assumes the remainder of saNavigationGuard has no unwanted side-effects, which is not necessarily true []