Spying on JavaScript Methods with Jasmine

Written by: Clemens Helm
5 min read
Stay connected

https://fast.wistia.com/embed/medias/xfas7bz2dm.jsonphttps://fast.wistia.com/assets/external/E-v1.js

This is the 17th Testing Tuesday episode. Every week we will share our insights and opinions on the software testing space. Drop by every Tuesday to learn more! Last week we introduced testing JavaScript with Jasmine.


Spying on JavaScript methods using Jasmine

Jasmine is made for unit testing. Unit tests are supposed to test only one component of your application. A component can be a function, an object, a module, basically everything self-contained that acts like a black box to the outside world. You usually want to avoid that your unit tests fail because another component failed. That's why you want to test your components in isolation as much as possible.

For example, you may not want to send data to another server in your unit tests or you don't want to manipulate a page's DOM. But you want to make sure that the components which are responsible for these tasks get called correctly. How can we do that?

Jasmine provides a feature called spies. A spy listens to method calls on your objects and can be asked if and how a method got called later on.

In this screencast, we show you how you can use spies to check if methods got called. We check if data gets sent to the server without ever performing a request by spying on jQuery's ajax method.

Up next Testing Tuesday: How to test asynchronous JavaScript with Jasmine

Next week we'll show you how to test asynchronous JavaScript operations with Jasmine. If you've got any questions or suggestions, please leave us a comment!

Further info:

Transcript

Spying on Javascript methods with Jasmine

Intro

Ahoy and welcome! My name is Clemens Helm and you're watching Codeship Testing Tuesday #17! Last week we introduced Jasmine which is a library for Javascript unit testing. We wrote some specs and asserted expectations. Today we'll spy on methods of objects to see if they get called or not.

Screencast

Jasmine is made for unit testing. Unit tests are supposed to test only one component of your application. A component can be a function, an object, a module, basically everything self-contained that acts like a black box to the outside world. You usually want to avoid that your unit tests fail because another component failed. That's why you want to test your components in isolation as much as possible.

For example, you don't want to send data to another server in your unit tests or you don't want to manipulate a page's DOM. But you want to make sure that the components which are responsible for these tasks get called correctly. How can we do that?

Jasmine provides a feature called spies. A spy listens to method calls on your objects and can be asked if and how a method got called later on. Let's say we've got a web frontend application that should send very important information to our web server. We've already got a method for this:

function VeryImportantInformation () {};
VeryImportantInformation.send = function (information) {
};

We can't actually test that the right request gets sent to the server in the unit test, but we know that we want to accomblish it using jQuery's ajax method. So we can just put a spy on this method to see if it called with the right argument:

describe("very important information", function () {
  it("shoud be sent", function () {
    spyOn(jQuery, "ajax");
  });
});

This spy will replace the ajax method with a stub that tracks if the method got called. The actual ajax method won't be called anymore, but that's what we want, because we want to test this behavior in isolation.

Let's add the method call now:

describe("very important information", function () {
  it("shoud be sent", function () {
    spyOn(jQuery, "ajax");
    VeryImportantInformation.send({i_am: "the walrus"});
  });
});

After the method call we want to check if the ajax method actually got called with the right parameters:

describe("very important information", function () {
  it("shoud be sent", function () {
    spyOn(jQuery, "ajax");
    VeryImportantInformation.send({"i am": "the walrus"});
    expect(jQuery.ajax).toHaveBeenCalledWith({
      method: "POST",
      url: "/important_information",
      data: {"i am": "the walrus"}
    });
  });
});

So let's open the SpecRunner.html file now to see what's the current status of our method. Jasmine fails saying that the ajax method was never called with the given values. So our spy did its job well and found out that we never called the ajax method.

Let's implement the send method the easiest way possible to get our spec working:

function VeryImportantInformation () {};
VeryImportantInformation.send = function (information) {
  jQuery.ajax({
    method: "POST",
    url: "/important_information",
    data: {"i am": "the walrus"}
  });
};

And this makes the spec work. But hold on, we just hardcoded the test data and didn't pass the actual information! We could write another spec with different data to make it impossible to implement the method this way. But I usually like to use random data instead, because it is less effort to write. Let's rewrite our spec to use random values:

describe("very important information", function () {
  it("shoud be sent", function () {
    spyOn(jQuery, "ajax");
    var information = {"i am": Math.random()};
    VeryImportantInformation.send(information);
    expect(jQuery.ajax).toHaveBeenCalledWith({
      method: "POST",
      url: "/important_information",
      data: information
    });
  });
});

Now our spec fails of course. To make it work again, we could try to pass Math.random() instead of "the walrus".

VeryImportantInformation.send = function (information) {
  jQuery.ajax({
    method: "POST",
    url: "/important_information",
    data: {"i am": Math.random()}
  });
};

But this fails as well, because Math.random() generates a different value everytime it is called. So the only way we can make the spec work now is by passing our information to the ajax call:

VeryImportantInformation.send = function (information) {
  jQuery.ajax({
    method: "POST",
    url: "/important_information",
    data: information
  });
};

Now our spec works!

You might be wondering why I didn't implement it this way in the first place. When I develop software, I write my tests first and then I try to make these tests work with as simple code as possible. I try to implement it wrong or incomplete deliberately, because this way I can prove that my tests aren't sufficient yet. Working this way makes your test suite rock-solid and it's very unlikely that flaws sneak into your application.

To sum up, when should you use spies and when not?

I think spies are handy everytime you deal with 3rd party libraries or with code you can't test entirely because it breaks the scope of your tests. But one problem of spies is, that they are tightly coupled to your implementation. For example, we could use jQuery's post method instead of the ajax method:

VeryImportantInformation.send = function (information) {
  jQuery.post("/important_information", information);
};

Even though the post method calls the ajax method internally, our spec fails now, because it expected additional arguments:

Expected spy ajax to have been called with [ { method : 'POST', url : '/important_information', data : { i am : 0.9397278234828264 } } ] but actual calls were [ { url : '/important_information', type : 'post', dataType : undefined, data : { i am : 0.9397278234828264 }, success : undefined } ]

So spies make it harder to change your code later on. That's why it can become cumbersome to work with them if your application changes frequently.

Outro

This was it for today, but there will be more on Jasmin next Testing Tuesday. We'll explore how to test asynchronous Javascript operations. Don't miss out on that and of course – always stay shipping!

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.

Loading form...
Your ad blocker may be blocking functionality on this page. Please disable for an improved experience.