Writing Unit Tests for Angular Components

Browser-based acceptance tests are slow and brittle. Even though you’ve eschewed starting an actual browser for each test by using PhantomJS, you still have to start our server and have it serve pages to the headless browser. Further, our tests rely on DOM elements and CSS classes to locate content used to verify behavior. You may need (or want) to make changes to the view that don’t break functionality, but break our tests.

Although we don’t want to abandon acceptance tests—after all, they are the only tests we have that exercise the system end to end—we need a way to test isolated bits of functionality (commonly called unit tests). In Rails, you have model tests and controller tests that allow you to do that for our server-side code. Unfortunately, Rails doesn’t provide any help for unit testing our client-side code.

In a classic Rails application, there isn’t much client-side code, so we are comfortable not explicitly testing it. In our more modern app, where a nontrivial amount of logic is written in JavaScript, the lack of unit testing will be a problem. For example, in the Angular app that powers the typeahead search you built in Chapter 6, Build a Dynamic UI with Angular, the search function has logic to prevent doing a search if the search term is fewer than three characters. It’s simple code, but there should be a test for it.

In this section, you’ll set up a means of writing and running unit tests for your Angular code. You’ll use Karma to run the tests, which you’ll write using Jasmine.[56] Both are common tools in the JavaScript testing ecosystem. You’ll also use the Testdouble.js[57] library to stub the various functions and objects Angular is providing to our code so that you can test in isolation. Because of how Angular is structured, this will be surprisingly straightforward.

First, let’s get set up for testing by installing Karma, Jasmine, and Testdouble.js, and then arranging to run our unit tests with Rake.

Set Up Karma, Jasmine, Testdouble.js, and the Rake Tasks

Setup instructions are always prescriptive, but it’s good to know why we are installing so many tools. Karma is a test runner that is agnostic of both how we write our tests and how we assemble our JavaScript. Jasmine is a testing library that contains various functions we can use to write our tests. In Rails, RSpec is both test runner and testing library. While Jasmine does include a basic test runner, it cannot use Webpack to assemble our dependencies. Karma can.

Karma has another advantage over Jasmine as a test runner. Jasmine’s test runner uses Node to execute our JavaScript, whereas Karma uses a browser. Since our Angular code is destined to run in a browser, it would be a better test of it to use a browser. Fortunately, we’ve already seen the advantages of PhantomJS as a headless browser, and we can use it with Karma as well.

Our setup has a lot of moving parts: Karma uses Webpack to assemble our JavaScript, which it combines with our testing code before sending it to PhantomJS to execute. But, it will all work and be the closest simulation of the real-world execution environment as possible. Let’s get installing!

Karma, Jasmine, and Testdouble.js are only needed for development and testing. If these were Ruby gems, we’d put them in the development group in our Gemfile. We can accomplish the same thing by using devDependencies in our package.json. But, rather than edit the file ourselves, we can manage it with yarn by using the --dev flag to yarn add.

 > yarn add jasmine \
  jasmine-core \
  karma \
  karma-jasmine \
  karma-phantomjs-launcher \
  karma-webpack \
  testdouble \
  --dev
 
 command output

Because yarn add also installs the library being added, we are now ready to get our testing environment set up. First, we’ll need a basic configuration file for Karma. As you are coming to learn about the JavaScript ecosystem, there are very few defaults, so basic things must be explicitly specified. In the case of Karma, we have to tell it what testing library we are using, where our JavaScript tests are located, and what browser to use to execute them. We must also point karma-webpack at our Webpack configuration so it can use Webpack to preprocess the files.

Create the directory spec/javascript and file spec/javascript/karma.conf.js like so:

7_testing/40-setup-karma/shine/spec/javascript/karma.conf.js
 module.exports = ​function​(config) {
  config.set({
  frameworks: [​'jasmine'​],
  files: [
 '**/*.spec.js'
  ],
  preprocessors: {
 '**/*.spec.js'​: [ ​'webpack'​ ]
  },
  webpack: require(​'../../config/webpack/test.js'​),
  browsers: [​'PhantomJS'​] })
 }

You’ll also need to make a small change to the Webpack configuration generated by Webpacker. In config/webpack/shared.js, you’ll need to change how the ManifestPlugin is configured. This plugin generates a JSON file at runtime so that Rails knows where all the Webpack-generated content is. Because of various incompatibilities with Karma and Webpack, we need to configure this plugin to not write the manifest file during tests.

7_testing/40-setup-karma/shine/config/webpack/shared.js
 plugins​:​ [
 new​ webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
 new​ ExtractTextPlugin(
  env.NODE_ENV === ​'production'​ ? ​'[name]-[hash].css'​ : ​'[name].css'​),
 new​ ManifestPlugin({
  publicPath: output.publicPath,
» writeToFileEmit: env.NODE_ENV !== ​'test'
  })
 ],

To validate this configuration, let’s create a basic test that always passes in spec/javascript/canary.spec.js. Don’t worry about the Testdouble stuff in there for the moment—I’ll explain that shortly. We want to make sure our tests can pull in third-party dependencies to validate our configuration.

7_testing/40-setup-karma/shine/spec/javascript/canary.spec.js
 import​ td from ​"testdouble/dist/testdouble"​;
 
 describe(​"JavaScript testing"​, ​function​() {
  it(​"works as expected"​, ​function​() {
 var​ mockFunction = td.​function​();
 
  td.when(mockFunction(42)).thenReturn(​"Function Called!"​);
 
  expect(mockFunction(42)).toBe(​"Function Called!"​);
  });
 });

We can run that test using the karma command-line application, which is installed in node_modules/.bin. Yarn provides a simpler way to execute command-line applications installed via Yarn, which is to use $(yarn bin)/karma, like so:

  > $(yarn bin)/karma --version
 Karma version: 1.7.0

Let’s try running our test:

 > NODE_ENV=test $(yarn/bin)/karma start spec/javascript/karma.conf.js \
  --single-run \
  --log-level=error \
  --fail-on-empty-test-suite
 PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 1 SUCCESS (0.004 secs…

Your version numbers might not match mine, but you should see the message, “Executed 1 of 1 SUCCESS,” which indicates our test was executed and passed. The --single-run switch means we want Karma to execute our tests and exit. (By default, it starts a web server that runs our tests in a browser.) --log-level makes an attempt to reduce the output of the command, and --fail-on-empty-test-suite is a sanity check that Karma has actually found our tests. Note that we explicitly set NODE_ENV to test, which triggers some test-only configuration in Webpack.

As mentioned earlier, let’s put this into a rake task so we don’t have to remember this long invocation. Doing so will also allow us to include our JavaScript tests as part of our build when someone types rake. Create the file lib/tasks/karma.rake like so:

7_testing/40-setup-karma/shine/lib/tasks/karma.rake
 desc ​"Run unit tests of JavaScript code with Karma"
 task ​:karma​ ​do
  ENV[​"NODE_ENV"​] = ​"test"
  mkdir_p ​"spec/fake_root"
  sh(​"$(yarn bin)/karma start spec/javascript/karma.conf.js "​ +
 "--single-run --log-level=error --fail-on-empty-test-suite"​)
 end
 task ​:default​ => ​:karma

This does exactly what we were doing on the command line: ensures we are in our app’s root directory and runs karma from node_modules/.bin. You can try it by running rake karma and if you just run rake, you should see that these tests are run as a part of that.

With our testing libraries installed, let’s write a unit test of our existing Angular code.

Write a Unit Test for the Angular Code

We’re going to write a complete unit test for CustomerSearchComponent, which will test its behavior when created and the behavior of the search function. Let’s create spec/javascript/CustomerSearchComponent.spec.js and just write out what we want to test using describe and it.

7_testing/40-setup-karma/shine/spec/javascript/CustomerSearchComponent.spec.js
 describe(​"CustomerSearchComponent"​, ​function​() {
  describe(​"initial state"​, ​function​() {
  it(​"sets customers to null"​);
  it(​"sets keywords to the empty string"​);
  });
 
  describe(​"search"​, ​function​() {
  describe(​"A search for 'pa', less than three characters"​, ​function​() {
  it(​"sets the keywords to be 'pa'"​);
  it(​"does not make an HTTP call"​);
  });
  describe(​"A search for 'pat', three or more characters"​, ​function​() {
  describe(​"A successful search"​, ​function​() {
  it(​"sets the keywords to be 'pat'"​);
  it(​"sets the customers to the results of the HTTP call"​);
  });
  describe(​"A search that fails on the back-end"​, ​function​() {
  it(​"sets the keywords to be 'pat'"​);
  it(​"leaves customers as null"​);
  it(​"alerts the user with the response message"​);
  });
  });
  });
 });

This roughly describes the behavior of CustomerSearchComponent, so all we need to do is fill in each of the it calls with our tests. To do that, we’ll need access to CustomerSearchComponent, which is currently defined inside app/javascript/packs/customer.js. You could try to require that file in our tests, but doing so won’t give us access to CustomerSearchComponent because it’s not being exported. In JavaScript, unlike Ruby, you have to explicitly export anything you want visible outside a file.

app/javascript/packs/customer.js also has code we don’t want to deal with for a unit test, such as the NgModule configuration. Let’s extract the code for CustomerSearchComponent out of this file, so we can load only that class for our unit test. This is much more like how we manage our Ruby files and will seem natural. After we do that, we’ll write our first tests, and then learn about mocking CustomerSearchComponent’s dependencies using TestDouble.js.

Extracting CustomerSearchComponent Into Its Own File

Webpacker’s intent is for each pack to have a file in app/javascript/packs, which we are doing now, but to put the meat of any code into app/javascript. The idea is that each pack is a separate js file that the browser will include. Webpack constructs each of those files for us, and the construction of those files might include re-usable modules we write, which are stored in app/javascript.

This is a big difference from how Sprockets works—Sprockets just creates a single file that has everything in it. Webpack produces multiple files that we include only when needed. What this means for us is that we need to move some code around into this structure, which meshes perfectly with our desire to extract code for testing.

Since the code in app/javascript/packs/customers.js that isn’t our CustomerSearchComponent is just about setting up the Angular app itself, we’ll leave that in there and only extract the code for CustomerSearchComponent. This is a good rule of thumb for Angular apps with Webpacker: component code should go in app/javascript and app-wide setup should stay in app/javascript/packs.

This means that our app/javascript/packs/customers.js should look like so:

7_testing/50-angular-unit-test/shine/app/javascript/packs/customers.js
»import​ { NgModule } from ​"@angular/core"​;
 import​ { BrowserModule } from ​"@angular/platform-browser"​;
 import​ { FormsModule } from ​"@angular/forms"​;
 import​ { platformBrowserDynamic } from ​"@angular/platform-browser-dynamic"​;
»import​ { HttpModule } from ​"@angular/http"​;
»import​ { CustomerSearchComponent } from ​"CustomerSearchComponent"​;

Note that we’ve slightly changed our imports. customers.js no longer needs Component or Http, so we can remove those from our import. We’ve also added an import for CustomerSearchComponent.

The Webpack configuration that Webpacker set up for us means that code like import { CustomerSearchComponent} from "CustomerSearchComponent" will look for the file app/javascript/CustomerSearchComponent/index.js or index.ts. This might seem odd, but as we’ll see in Storing View Templates in HTML Files, we can further extract our component into files, and having them in a directory will make it easy to keep everything organized.

Given this convention, let’s create the directory app/javascript/CustomerSearchComponent and create index.ts in there like so:

7_testing/50-angular-unit-test/shine/app/javascript/CustomerSearchComponent/index.ts
»import​ { Component } ​from​ ​"@angular/core"​;
»import​ { Http } ​from​ ​"@angular/http"​;
 
 var​ CustomerSearchComponent = Component({
 
 // Same code as before...
 
 });
 
»export​ { CustomerSearchComponent };

I’ve highlighted the two bits of new code you’ll need to add. The first bit probably looks familiar—it brings in the Angular components our code needs. This feels like duplication compared to how things work with Ruby, and it highlights a difference in how JavaScript treats modules. In JavaScript, everything is isolated to the file, and there isn’t really any global state. This is great for encapsulation and code management, but it does require us to be explicit about what external libraries and functions any given file needs.

The second bit of highlighted code is at the end and shows how we export our code to other files that require this file. module.exports is special to JavaScript and Webpack. It’s the glue between our extracted code and what is set up by the import statement in app/javascript/packs/customers.js.

One last oddity is that we named our file with a ts extension, which is for files containing TypeScript, instead of a js one. I mentioned briefly in Install Angular that we were not going to use TypeScript, and generally we aren’t. Since many examples online are in TypeScript, you may want to learn it and start using it after you get through this book. Because TypeScript is a superset of JavaScript, you can start using it whenever you want, since the configuration Webpacker created already supports TypeScript. By using ts files now, your switch to TypeScript later will be much easier.

Now that we’ve extracted CustomerSearchComponent into its own class, we can run our browser-based tests to make sure we haven’t broken anything and then move on to writing some tests for it.

Writing Simple Unit Tests

Let’s start with the tests around the initial state of the component. This allows us to see how we create the component and inspect it. First, we’ll use import to bring in CustomerSearchComponent:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
»import​ ​"./SpecHelper"​;
»import​ { CustomerSearchComponent } from ​"CustomerSearchComponent"​;

Note that we’ve required SpecHelper.js. We’re borrowing this pattern from RSpec, and this file, which we’ll create in a moment, can handle cross-cutting set up which, unfortunately, we need. If you recall, we had to import a bunch of libraries to set up a shared global state. Those are currently in app/javascript/hello_angular/polyfills.ts. We need those in our tests, too, but rather than have that inside each test, we’ll centralize them in one file called SpecHelper.js.

One thing that’s not obvious now, however, is that one of the polyfills—zone.js—cannot be imported more than once. Since we cannot conditionally import a file, we need to make it easy to import the polyfills that are safe to import multiple times. To that end, we’ll create app/javascript/polyfills/no_zonejs.ts that has all the polyfills save for zone.js’s. We’ll then import that file in app/javascript/polyfills/index.ts, where we’ll also import zone.js. Finally, our SpecHelper.js can import only polyfills/no_zonejs.ts. Whew! Let’s see the code changes.

First, here’s what app/javascript/polyfills/index.ts should look like:

7_testing/50-angular-unit-test/shine/app/javascript/polyfills/index.ts
 import​ ​'./no_zonejs'​;
 import​ ​'zone.js/dist/zone'​;

Next is app/javascript/polyfills/no_zonejs.ts:

7_testing/50-angular-unit-test/shine/app/javascript/polyfills/no_zonejs.ts
 import​ ​'core-js/es6/symbol'​;
 import​ ​'core-js/es6/object'​;
 import​ ​'core-js/es6/function'​;
 import​ ​'core-js/es6/parse-int'​;
 import​ ​'core-js/es6/parse-float'​;
 import​ ​'core-js/es6/number'​;
 import​ ​'core-js/es6/math'​;
 import​ ​'core-js/es6/string'​;
 import​ ​'core-js/es6/date'​;
 import​ ​'core-js/es6/array'​;
 import​ ​'core-js/es6/regexp'​;
 import​ ​'core-js/es6/map'​;
 import​ ​'core-js/es6/set'​;
 import​ ​'core-js/es6/reflect'​;
 import​ ​'core-js/es7/reflect'​;

You should remove app/javascript/hello_angular/polyfills.ts and update both app/javascript/hello_angular/index.ts and app/javascript/packs/customers.js to reference the new file. Here’s hello_angular’s:

7_testing/50-angular-unit-test/shine/app/javascript/hello_angular/index.ts
»import​ ​'polyfills'​;
 
 import​ { platformBrowserDynamic } ​from​ ​'@angular/platform-browser-dynamic'​;
 import​ { AppModule } ​from​ ​'./app/app.module'​;
 
 platformBrowserDynamic().bootstrapModule(AppModule);

And here’s customers.js:

7_testing/50-angular-unit-test/shine/app/javascript/packs/customers.js
 import​ ​"polyfills"​;

And finally, we can create spec/javascript/SpecHelper.js to only import those polyfills that can be safely imported more than once:

7_testing/50-angular-unit-test/shine/spec/javascript/SpecHelper.js
 import​ ​"polyfills/no_zonejs"​;

To write your test, you need an instance of your component. I didn’t talk much about what the Component and Class functions actually do. Under the covers, they create a JavaScript class, meaning you can create an instance of CustomerSearchComponent by using new, and anything you pass to the constructor will be sent along to the constructor function you defined.

Right now we don’t need to worry about the constructor arguments, so we’ll create our instance via new CustomerSearchComponent(). In order to only do that once, we’ll use Jasmine’s beforeEach function, which works just like RSpec’s before method. Because of JavaScript’s scoping rules, we need the variable for our component to be declared outside our describe function, so we’ll declare it at the top of the file, and then assign it inside beforeEach:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
»var​ component = ​null​;
 
 describe(​"CustomerSearchComponent"​, ​function​() {
» beforeEach(​function​() {
» component = ​new​ CustomerSearchComponent();
» });

Now, we can fill in our two tests. All they’ll do is access the customers and keywords properties of our instance and make sure they are set to their initial values (note this code appears right after the beforeEach code we just wrote):

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
  describe(​"initial state"​, ​function​() {
  it(​"sets customers to null"​, ​function​() {
» expect(component.customers).toBe(​null​);
  });
  it(​"sets keywords to the empty string"​, ​function​() {
» expect(component.keywords).toBe(​""​);
  });
  });

If you run rake karma, you’ll see that our two assertions ran and our test passed:

 > bin/rails karma
 tons of output
 PhantomJS 2.1.1 (Mac OS X 0.0.0):
  Executed 3 of 10 (skipped 7) SUCCESS (0.011 secs / 0.005 secs)

Now, let’s test the search function of our component. This requires setting up some test doubles for Angular’s HTTP library.

Using Test Doubles to Stub Dependencies

Because we’re writing unit tests, we want to test in isolation, meaning we don’t want to actually make HTTP calls in our test. To avoid that, we’ll stub out the Http class we’re using at runtime. Because of how Angular components are designed, we can easily pass in our own implementation of Http to the constructor of CustomerSearchComponent. The question, then, is where do we get our test implementation?

Although Angular provides a system for mocking out HTTP calls, it is highly complex and will make our job of testing in isolation quite difficult, as we’ll need to do fairly extensive setup work in our tests to make it work. Even if we used TypeScript, we’d still have a lot of conceptual overhead in getting this working, so let’s instead use generic test doubles. This won’t be simple, but it will be simpler.

The test double library is called Testdouble.js and you installed it earlier. It provides everything you need to make a mock HTTP object. Let’s start with our first test of search, which will do a search for “pa” and assert that we don’t make any HTTP calls. In our case, our code would execute http.get() if it did perform a search, so we want a test double that responds to get() and a way to check that it wasn’t called.

First, we’ll bring in the Testdouble.js library:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
»import​ td from ​"testdouble/dist/testdouble"​;

Note the slight change in syntax for import: we aren’t using curly braces. This is because we want to import everything that is exported, and not just a particular function or class.

Next, we’ll create a beforeEach block for our tests that creates a test double for Angular’s HTTP and passes it into our component when we create it:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 describe(​"search"​, ​function​() {
»var​ mockHttp = ​null​;
» beforeEach(​function​() {
» mockHttp = td.object([​"get"​]);
» component = ​new​ CustomerSearchComponent(mockHttp);
» });

The object method on td creates an object that can respond to the methods given in the array. In our case, the code td.object(["get"]) means “create an object that has a get function.” In a later test, you’ll use more functions of Testdouble.js to specify the behavior of our mock object, but for now we just need it to exist.

Now, let’s implement the first test that asserts that the keywords property gets set to our search string when we call search:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 it(​"sets the keywords to be 'pa'"​,​function​() {
» component.search(​"pa"​);
» expect(component.keywords).toBe(​"pa"​);
 });

Next, we’ll write a test that explicitly requires that the get method on http was not called. This is how we’ll assert that a search for a string shorter than three characters doesn’t hit the back end.

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 it(​"does not make an HTTP call"​, ​function​() {
» component.search(​"pa"​);
» td.verify(mockHttp.get(), { times: 0 });
 });

This is using the verify function of Testdouble, which is usually used to check that a method was called. In our case, because we want to make sure it wasn’t, we use { times: 0 }.

Running our tests, they should pass. This wasn’t too bad, but things are about to get a bit complex as we move onto the next test, where we need to arrange for our mock HTTP object to return results.

Stubbing Complex Interactions to Test HTTP

Let’s write the test for a successful search of the term “pat.” You can set up our beforeEach in much the same way, except now you have to provide an implementation of get.

If you look at the contract of the get function in Angular’s HTTP library, it accepts a URL and returns an observable (which I talked about in Get Data from the Back End). You then call subscribe on that observable, passing it two functions: one for a successful result and one for an error. The success function is expecting the data from the back end, and it’s going to call json() on that data to parse it. Whew!

First, we’ll create a test double for the response passed to our success function. We’ll create it using object, just as we did with http, but we’ll use td.when to specify the behavior of the json function. (Note this is all new code)

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 describe(​"A search for 'pat', three or more characters"​, ​function​() {
 var​ mockHttp = ​null​;
 var​ customers = [
  {
  id: 1,
  created_at: (​new​ Date()).toString(),
  first_name: ​"Pat"​,
  last_name: ​"Jones"​,
  username: ​"pj"​,
  email: ​"pjones@somewhere.net"
  },
  {
  id: 2,
  created_at: (​new​ Date()).toString(),
  first_name: ​"Pat"​,
  last_name: ​"Jones"​,
  username: ​"pj"​,
  email: ​"pjones@somewhere.net"
  },
  ];
  beforeEach(​function​() {
 var​ response = td.object([​"json"​]);
  td.when(response.json()).thenReturn({ customers: customers });
  mockHttp = td.object([​"get"​]);
  component = ​new​ CustomerSearchComponent(mockHttp);

We’ve declared customers outside beforeEach because we’ll eventually assert that our component’s customers property was assigned these customers. Next, we’ll create a test double for our observable.

Creating a test double is trickier because you need to define the subscribe function as “calls our success callback.” You can do this by using td.callback along with td.when. Because subscribe takes two callbacks, and we only want one called, we’ll use td.matchers.isA(Function) as the second argument. This tells Testdouble.js to not call the second callback, but to fail the test if something other than a function is passed to subscribe’s second parameter.

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 beforeEach(​function​() {
 var​ response = td.object([​"json"​]);
  td.when(response.json()).thenReturn({ customers: customers });
 
»var​ observable = td.object([​"subscribe"​]);
» td.when(observable.subscribe(
» td.callback(response),
» td.matchers.isA(Function))).thenReturn();

Now when our production code calls subscribe, the test double called observable will first make sure that the two arguments passed to it are functions. If they are, it will then execute the first function, passing it response. It will not execute the second function, and simulates a successful response. This part is rather tricky to get your head around, but it’s ultimately the easiest way to deal with callbacks in your test code and maintain isolation in your testing. Another way to read this might be “when we call subscribe on observable, call the callback passed as the first parameter with our mock response, and make sure the second parameter is also a function.”

Last, we need to configure our mock HTTP object to return this mock observable when get is called:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 mockHttp = td.object([​"get"​]);
 
»td.when(mockHttp.get(​"/customers.json?keywords=pat"​)).thenReturn(observable);
 
 component = ​new​ CustomerSearchComponent(mockHttp);

With all that setup, we can now write our two tests (note this code appears below the beforeEach block where the code above was placed—consult the downloaded code to see the entire picture):

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 describe(​"A successful search"​, ​function​() {
  it(​"sets the keywords to be 'pat'"​,​function​() {
» component.search(​"pat"​);
» expect(component.keywords).toBe(​"pat"​);
  });
  it(​"sets the customers to the results of the HTTP call"​, ​function​() {
» component.search(​"pat"​);
» expect(component.customers).toBe(customers);
  });
 });

Running the tests, we should see that they pass. We’ve now successfully mocked the HTTP calls and can test in complete isolation! Also note that you didn’t have to do anything Angular-specific. This is an interesting side effect of the way Angular wants us to write code. Our code has no real ties to Angular, even though it uses some of Angular’s classes. In the end, our code is just some JavaScript that you can execute.

Next, let’s test the error case, which will create some complications around our use of window.

Testing the Error Case and Removing window

In Get Data from the Back End, we decided to use window.alert as the means of letting the user know that an error had occurred. As you’ll see, this is going to be a slight problem. To see the problem, let’s get our test setup. We’ll set it up much as we did the success case, but we’ll arrange to have Testdouble.js call the error callback instead. The main difference in setup is how you configure the call to subscribe: you’ll pass td.callback(response) as the second argument, to make sure the error callback is called when the test runs.

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 describe(​"A search that fails on the back-end"​, ​function​() {
» beforeEach(​function​() {
»var​ response = ​"There was an error!"​;
»var​ observable = td.object([​"subscribe"​]);
»
» td.when(observable.subscribe(
» td.matchers.isA(Function),
» td.callback(response))).thenReturn();
»
» mockHttp = td.object([​"get"​]);
» td.when(mockHttp.get(​"/customers.json?keywords=pat"​)).thenReturn(observable);
»
» component = ​new​ CustomerSearchComponent(mockHttp);
» });
  it(​"sets the keywords to be 'pat'"​,​function​() {
» component.search(​"pat"​);
» expect(component.keywords).toBe(​"pat"​);
  });
  it(​"leaves customers as null"​, ​function​() {
» component.search(​"pat"​);
» expect(component.customers).toBe(​null​);
  });
  it(​"alerts the user with the response message"​,​function​() {
»// ???
  });
 });

You can already see what the problem will be, since it’s not clear how to test that window.alert was called with our error message. When you run rake karma, you can see that the error message we configured is getting printed out (as Karma’s output shows whatever PhantomJS receives from a call to window.alert).

 > bundle exec rake karma
 lots of output
 ALERT: 'There was an error!'
 PhantomJS 2.1.1 (Mac OS X 0.0.0):
  Executed 10 of 10 SUCCESS (0.014 secs / 0.012 secs)

The test passes because we aren’t asserting anything, but we need to figure out how to assert that the call to window.alert happened. We can do that by using Testdouble to replace the alert function on window with one that we can observe. The replace function from Testdouble will do just this:

7_testing/50-angular-unit-test/shine/spec/javascript/CustomerSearchComponent.spec.js
 it(​"alerts the user with the response message"​,​function​() {
» td.replace(window,​"alert"​);
» component.search(​"pat"​);
» td.verify(window.alert(​"There was an error!"​));
 });

We’re using the more typical form of td.verify, and this code does more or less what it appears to. It will fail the test if window.alert was called with an argument other than “There was an error!”

This is obviously brittle. Our test has to much with a global variable in order to work. Angular 1 provided an abstraction for us—$window—but this is not available in later versions of Angular. You could create a service class that handles the error notification, and inject that the same way you did with Http. (you’ll see this sort of thing in action in Extracting Reusable Code into Services.) It would probably be a better user experience to stop using window.alert and design a real alerting component using Bootstrap.

We won’t do that here, as you now know enough to both design such a component and—now—write a test for it. For the latter, you could check that CustomerSearchComponent exposed the right error message to its view, much like how we’ve been checking that customers has been set appropriately.

Now that you can successfully write unit tests using Jasmine (running them with Karma), and mock out Angular-provided classes easily with Testdouble.js, you have the tools you need to test your JavaScript code. Our mocking of Http was complex enough that you should be able to tackle any test you need.