I recently wrote a series of posts on testing React apps and a series on using phpunit for testing WordPress plugins. Those covered testing brand new code and writing it in a way that is testable. One of the tricky things about adopting tests in a legacy code base is that the code is often written in a way that makes testing harder.
In this article, I’m going to look at two ways that jQuery is hard to test. First I will show how to use mocks to artificially isolate your code from jQuery itself. Then I will give an example with code that runs on a click and code used for making AJAX requests.
Quick Introduction To Jest Mocks
One of the things that is tricky about unit testing is that not all code can be written to be perfectly isolated from global systems such as DOM events or WordPress hooks. Mocking libraries help you artificially remove systems that are not what the current test is covering. For example, 10up/WP_Mock can be used to replace the WordPress plugins API with testing mocks.
This article is about testing jQuery with Jest. Jest is developed by Facebook and is generally used to test React apps, but it can be used with other test libraries. One great feature of Jest is its mocking capabilities. The simplest use of Jest mocks is to count the number of times a function is called. If your test is a function that calls another function, you just need to know that function is called.
Let’s look at a test for a function that takes an array of items and then applies the same callback to each of them. In this snippet I have updateItems — the function to test — and updatePosts() which uses that function to pass an array of posts to updatePost(). Later in this article, I look at how to test jQuery.ajax() calls. For now, I’m just worried about making sure my updateItems() dispatches the callback:
Now, let’s look at the test. I’m not even going to give it mock posts at this point, just an array with three items and then assert my callback was called three times:
The key line here is line 4. On that line, I create a function called “callback” using jest.fn(). As a result, I can count the times it’s called using the callback.mocks.calls.length.
That tests that my function was called the right number of times. It does not show me that it got the right data. For that we can use the calledWith utility function of jest.fn():
Separate Concerns First
One of the biggest obstacles to adopting testing in a legacy code base is that your code may not be easy to test. Isolated unit testing may be impossible. You can still write tests of the DOM with a browser automation framework such as Cypress.io or similar. You can also use something like dom-testing-library to test the DOM.
But simple refactors can isolate your business logic from the DOM event system. Here is an example where jQuery is used to bind to a click event and then add or remove classes based on a condition:
You could render all or part of the DOM, simulate the click and then test if the DOM elements have the right classes. That’s slow and its testing a lot of things that are the responsibility of jQuery, not your code.
Your business logic is your business, jQuery’s event binding and dispatching system is not. The snippet of code I showed above does many things, a violation of the single responsibility principle. Let’s break it up into two functions. One function takes jQuery as a dependency and then executes the business logic. The other function’s responsibility is to wire the isolated logic that is encapsulated in the first function to jQuery’s event system.
Now let’s look at how to test this function with our business logic. Because we pass jQuery in as a dependency to the function, we could pass any function there. Such as a jest mock. Because we’re not going to be testing the effects of these functions — adding and removing classes from the right DOM elements — we just need to know that the functions are called.
The basic mock I showed before is not sufficient for this test. I say that for two reasons. First off it doesn’t have a constructor so the jQuery constructor call, which we’re not actually testing would throw an error. Second, we need to be able to count the calls to separate methods.
Here is a test that solves the first problem but not the second:
This will pass with a proper constructor. But, all we know is two methods of this object were called. Which ones? We don’t know, and that matters as in our test we need to make sure removeClass is called but addClass is not. That’s the business logic we’re testing.
The solution is to put those methods in their own variables, we can check:
Now we’re testing that the business logic leads to the right function being called. We’re not testing the effects, just our logic. Mission achieved.
What About The Event Binding?
I totally didn’t cover the actual event bindings. I don’t care.
First, I really doubt that I will have an issue there. If there is, that’s a big problem that will be surfaced by acceptance tests and integration tests built that run against a real website and simulate user interactions will fail hard if jQuery is not working properly and that gives me more confidence in my event bindings then any mock event I will create for tests. If the business logic is tested, I’m good.
Testing jQuery AJAX With Jest
Testing anything that involves an HTTP request can be tricky. Getting rid of side effects first is important. Breaking the business logic apart from the jQuery.ajax() API can allow for a similar testing strategy. Consider this jQuery AJAX usage with three callbacks:
This is pretty common, I copied it out of something I wrote a while ago. One way to think about testing this code would be to leave it as is, but come up with a way to mock the API. That doesn’t make sense to me if the API is covered by its own tests and jQuery AJAX has its own tests. Instead, think of it as three functions:
Here are three functions that we could use. Isolating them into functions means they can be reused, which is great. In addition, we can pass the two global dependencies — jQuery and Handlebars into the functions. These types of functions are not pure by design. The term “pure” in this context means a function with no side effects. These functions modify the DOM using global-scoped APIs and that’s fine if we can easily replace the global-scoped APIs with Jest mocks.
Here are tests that just check that the right function in the mock object are called. In one place — the argument for the error function — I am concerned that the right value is passed to that method, so I check with Jest’s expectToBeCalledWith. The other functions I’m mocking, I trust they work, as long as they are called. Calling them in the right order is my concern, calling them with the right data is my concern, what they do is not my concern.
What About The API Request?
For the most part, I don’t care for the same reasons I gave for the event bindings. API endpoints get their own isolated tests. Also, I have acceptance tests. If I was building an API client, then I would need to test it with mock responses. For that, I would use a mocking library for the AJAX requests.
I prefer Fetch to jQuery.ajax() for a few reasons — it’s built into the browser and works the same on the server and there is a really useful mocking library for it. I wrote a bit about how to write unit tests for the Fetch API here.