Chapter 6. Spies

As we’ve learned, Jasmine will let us test if things are working the way we want them to. We want to ability to check if functions have been called, and whether or not they’ve been called how we want them to be called. We specify how our code should work.

In Jasmine, a spy does pretty much what its name implies: it lets you spy on pieces of your program (and in general, the pieces that aren’t just variable checks). A little less exciting than James Bond, but still cool spying.

The Basics: Spying on a Function

Spying allows you to replace a part of your program with a spy. A spy can pretend to be a function or an object. When is this useful?

Let’s say that you have a class called Dictionary. It represents an English dictionary, and can return “hello” and “world”:

  var Dictionary = function() {};
  Dictionary.prototype.hello = function() {
      return "hello";
  };
  Dictionary.prototype.world = function() {
      return "world";
  };

And now let’s say that you have a class called Person, which should be able to output “hello world” using Dictionary (passed in as an argument). It might work like this:

  var Person = function() {};
  Person.prototype.sayHelloWorld = function(dict) {
      return dict.hello() + " " + dict.world();
  };

So, to get your Person to return “hello world,” you’d do something like this:

  var dictionary = new Dictionary;
  var person = new Person;
  person.sayHelloWorld(dictionary);  // returns "hello world"

You could, in theory, make the sayHelloWorld function return the string literal hello world, but you don’t want that—you want to make sure that your Person consults the Dictionary.

Spies will help you; you can spy on the dictionary and make sure that it’s used. Here’s how to do that:

  describe("Person", function() {
      it('uses the dictionary to say "hello world"', function() {
          var dictionary = new Dictionary;
          var person = new Person;
          spyOn(dictionary, "hello");  // replace hello function with a spy
          spyOn(dictionary, "world");  // replace world function with another
                                          spy
          person.sayHelloWorld(dictionary);
          expect(dictionary.hello).toHaveBeenCalled();  // not possible without
                                                           first spy
          expect(dictionary.world).toHaveBeenCalled();  // not possible without
                                                           second spy
      });
  });

Let’s go through this, line by line. First, we make our two objects. Then, we’ll spyOn the dictionary’s hello and world methods. This basically tells Jasmine to replace dictionary.hello and dictionary.world with spies. Very sneaky. Then we call person.sayHelloWorld and make sure that our dictionary’s methods were called.

Throwing this through the spec runner should give you positive results; your program should indeed have called the dictionary methods.

Why is this useful? Say you decide to make your dictionary Spanish instead of English:

  var Dictionary = function() {};
  Dictionary.prototype.hello = function() {
      return "hola";
  };
  Dictionary.prototype.world = function() {
      return "mundo";
  };

While sayHelloWorld will return different values, the exact same spec will succeed because the dictionary is consulted whether you’re using Spanish or English!

OK, maybe that’s not all you want. Maybe you want to make sure that +person.+pass[<phrase role=keep-together><literal>sayHelloWorld</literal></phrase>] is called with a specific dictionary. Jasmine has got your back. Take a look at this example spec:

  describe("Person", function() {
      it('uses the dictionary to say "hello world"', function() {
          var dictionary = new Dictionary;
          var person = new Person;
          spyOn(person, "sayHelloWorld");  // replace hello world function with
                                              a spy
          person.sayHelloWorld(dictionary);
          expect(person.sayHelloWorld).toHaveBeenCalledWith(dictionary);
      });
  });

As you may be able to read (Jasmine looks a lot like English!), this spy makes sure that sayHelloWorld’s argument is dictionary and not some other dictionary object. Run that through Jasmine, and it’ll tell you everything is good.

If you want to ensure that something isn’t called, it’s a lot like when you’re making sure a variable isn’t something: use .not. So, for example, if you want to make sure that a function isn’t called with a particular argument, you’d write this:

  expect(person.sayHelloWorld).not.toHaveBeenCalledWith(dictionary);

Calling Through: Making Your Spy Even Smarter

Simply using spyOn makes a spy function that knows whether something’s been called.

If you want to spy on a function and make sure that it still works, you can call through. This makes the spy even sneakier. All you have to do is add andCallThrough to your spyOn call:

  describe("Person", function() {
      it('uses the dictionary to say "hello world"', function() {
          var dictionary = new Dictionary;
          var person = new Person;
          spyOn(dictionary, "hello");  // replace hello function with a spy
          spyOn(dictionary, "world");  // replace world function with another
                                          spy
          var result = person.sayHelloWorld(dictionary);
          expect(result).toEqual("hello world");  // not possible without
                                                     calling through
          expect(dictionary.hello).toHaveBeenCalled();
          expect(dictionary.world).toHaveBeenCalled();
      });
  });

This spy function will do everything that the old function did, and it will be a good spy and let you see its inner workings.

Making Sure a Spy Returns a Specific Value

You can also make sure that a spy always returns a given value. Let’s say that you want the dictionary’s hello spy to speak French:

  it("can give a Spanish hello", function() {
      var dictionary = new Dictionary;
      var person = new Person;
      spyOn(dictionary, "hello").andReturn("bonjour");   // note this new piece
      var result = person.sayHelloWorld(dictionary);
      expect(result).toEqual("bonjour world");
  });

This can be useful if you want to make sure that, despite a changed function, everything else works well. You can also use this to see how your code performs if given bad output. In this example, for instance, dictionary.hello might be broken.

Replacing a Function with a Completely Different Spy

Spies can get even crazier. They can call through to a fake function, like so:

  it("can call a fake function", function() {
      var fakeHello = function() {
          alert("I am a spy! Ha ha!");
          return "hello";
      };
      var dictionary = new Dictionary();
      spyOn(dictionary, "hello").andCallFake(fakeHello);
      dictionary.hello();  // does an alert
  });

This means that you can test your code against, say, a buggy API.

Creating a New Spy Function

In the previous examples, we were building spies that replaced existing functions. It is sometimes useful to create a spy for a function that doesn’t yet exist. If you want to, say, give your Person a getName spy, you can do that by creating a new spy.

Where spyOn created a spy by “eating” an existing function, jasmine.createSpy doesn’t have to. It can make a new one:

  it("can have a spy function", function() {
      var person = new Person();
      person.getName = jasmine.createSpy("Name spy");
      person.getName();
      expect(person.getName).toHaveBeenCalled();
  });

Like other spies, spies created with jasmine.createSpy can have other methods chained onto them:

  person.getSecretAgentName = jasmine.createSpy("Name spy").andReturn("James
       Bond");

  person.getRealName = jasmine.createSpy("Name spy 2").andCallFake(function() {
          alert("I am also a spy! Ha ha!");
          return "Evan Hahn";
  });

Creating a New Spy Object

In addition to making a new spy function, you can also make a new spy object:

  var tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);

It can be used like this:

  tape.play();
  tape.rewind(10);

Basically, this creates an object called tape that has play, pause, stop, and rewind functions. All of those functions are spy functions and act just like the spies we’ve seen before.

This can be useful for testing whether your code calls an external API.