A Beginner’s Guide to Testing Functional JavaScript

M. David Green
Share

Functional programming and testing. Maybe you’ve given them a try in isolation, but somehow you never made either a part of your regular practice. They may sound innocent by themselves, but together testing and functional programming can create an irresistible temptation, almost compelling you to write cleaner, tighter, more maintainable code.

Testing Functional JavaScript — A crash test dummy sits at a computer with a mug of coffee

Well the good news is that working with both techniques together can offer some real advantages. In fact, once you’ve tasted how sweet this combination can be, you might find yourself as addicted to it as I am, and I’d be willing to bet that you’ll be coming back for more.

In this article, I’ll introduce you to the principles of testing functional JavaScript. I’ll show you how to get up and running with the Jasmine framework and build out a pure function using a test-driven approach.

Why Test?

Testing is about making sure that the code in your application does what you expect it to do, and keeps doing what you expect it to do when you make changes so you have a working product when you’re done. You write a test that defines your expected functionality under a defined set of circumstances, run that test against the code, and if the result isn’t what the test says it should be, you get a warning. And you keep getting that warning until you’ve fixed your code.

Then you get the reward.

And yes, it will make you feel good.

Testing comes in a lot of flavors, and there is room for healthy debate about where the borders are drawn, but in a nutshell:

  • Unit tests validate the functionality of isolated code
  • Integration tests verify the flow of data and the interaction of components
  • Functional tests look at the behavior of the overall application

Note: Don’t get distracted by the fact that there’s a type of testing called functional testing. That’s not what we’ll be focusing on in this article about testing functional JavaScript. In fact, the approach you’ll use for functional testing of the overall behavior of an application probably won’t change all that much whether or not you’re using functional programming techniques in your JavaScript. Where functional programming really helps out is when you’re building your unit tests.

You can write a test at any point in the coding process, but I’ve always found that its most efficient to write a unit test before writing the function you’re planning to test. This practice, known as test-driven development (TDD), encourages you to break down the functionality of your application before you start writing and determine what results you want from each section of code, writing the test first, then coding to produce that result.

A side benefit is that TDD often forces you to have detailed conversations with the people who are paying you to write your programs, to make sure that what you’re writing is actually what they’re looking for. After all, it’s easy to make a single test pass. What’s hard is determining what to do with all the likely inputs you’re going to encounter and handle them all correctly without breaking things.

Why Functional?

As you can imagine, the way you write your code has a lot to do with how easy it is to test. There are some code patterns, such as tightly coupling the behavior of one function to another, or relying heavily on global variables, that can make code much more difficult to unit test. Sometimes you may have to use inconvenient techniques such as “mocking” the behavior of an external database or simulating a complicated runtime environment in order to establish testable parameters and results. These situations can’t always be avoided, but it is usually possible to isolate the places in the code where they are required so that the rest of the code can be tested more easily.

Functional programming allows you to deal with the data and the behavior in your application independently. You build your application by creating a set of independent functions that each work in isolation and don’t rely on external state. As a result, your code becomes almost self-documenting, tying together small clearly defined functions that behave in consistent and understandable ways.

Functional programming is often contrasted against imperative programming and object-oriented programming. JavaScript can support all of these techniques, and even mix-and-match them. Functional programming can be a worthwhile alternative to creating sequences of imperative code that track the state of the application across multiple steps until a result is returned. Or building your application out of interactions across complex objects that encapsulate all of the methods that apply to a specific data structure.

How Pure Functions Work

Functional programming encourages you to build your application out of tiny, reusable, composable functions that just do one specific thing and return the same value for the same input every single time. A function like this is called a pure function. Pure functions are the foundation of functional programming, and they all share these three qualities:

  • Don’t rely on external state or variables
  • Don’t cause side effects or alter external variables
  • Always return the same result for the same input

Another advantage of writing functional code is that it makes it much easier to do unit testing. The more of your code that you can unit test, the more comfortably you can count on your ability to refactor the code in the future without breaking essential functionality.

What Makes Functional Code Easy to Test?

If you think about the concepts that we just discussed, you probably already see why functional code is easier to test. Writing tests for a pure function is trivial, because every single input has a consistent output. All you have to do is set the expectations and run them against the code. There’s no context that needs to be established, there are no inter-functional dependencies to keep track of, there’s no changing state outside of the function that needs to be simulated, and there are no variable external data sources to be mocked out.

There are a lot of testing options out there ranging from full-fledged frameworks to utility libraries and simple testing harnesses. These include Jasmine, Mocha, Enzyme, Jest, and a host of others. Each one has different advantages and disadvantages, best use cases, and a loyal following. Jasmine is a robust framework that can be used in a wide variety of circumstances, so here’s a quick demonstration of how you might use Jasmine and TDD to develop a pure function in the browser.

You can create an HTML document that pulls in the Jasmine testing library either locally or from a CDN. An example of a page including the Jasmine library and test runner might look something like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Jasmine Test</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine.min.css">
  </head>
  <body>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine-html.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/boot.min.js"></script>
  </body>
</html>

This brings in the Jasmine library, along with the Jasmine HTML boot script and styling. In this case the body of the document is empty, waiting for your JavaScript to test, and your Jasmine tests.

Testing Functional JavaScript — Our First Test

To get started, let’s write our first test. We can do this in a separate document, or by including it inside a <script> element on the page. We’re going to use the describe function defined by the Jasmine library to describe the desired behavior for a new function we haven’t written yet.

The new function we’re going to write will be called isPalindrome and it will return true if the string passed in is the same forwards and backwards, and return false otherwise. The test will look like this:

describe("isPalindrome", () => {
  it("returns true if the string is a palindrome", () => {
    expect(isPalindrome("abba")).toEqual(true);
  });
});

When we add this to a script in our page and load it a browser, we get a working Jasmine report page showing an error. Which is what we want at this point. We want to know that the test is being run, and that it’s failing. That way our approval-hungry brain knows that we have something to fix.

So let’s write a simple function in JavaScript, with just enough logic to get our test to pass. In this case it’s just going to be a function that makes our one test pass by returning the value expected.

const isPalindrome = (str) => true;

Yes, really. I know it looks ridiculous, but hang in there with me.

When the test runner again, it passes. Of course. But obviously this simple code does not do what we might expect a palindrome tester to do. We’ve written the minimal amount of code that makes the test pass. But we know that our code would fail to evaluate palindromes effectively. What we need at this point are additional expectations. So let’s add another assertion to our describe function:

describe("isPalindrome", () => {
  it("returns true if the string is a palindrome", () => {
    expect(isPalindrome("abba")).toEqual(true);
  });
  it("returns false if the string isn't a palindrome", () => {
    expect(isPalindrome("Bubba")).toEqual(false);
  });
});

Reloading our page now makes the test output turn red and fail. We get a messages saying what the problem is, and the test result turns red.

Red!

Our brains sense that there’s a problem.

Of course there is. Now our simple isPalindrome function that just returns true every time has been demonstrated not to work effectively against this new test. So let’s update isPalindrome adding the ability to compare a string passed in forward and backward.

const isPalindrome = (str) => {
  return str
    .split("")
    .reverse()
    .join("") === str;
};

Testing Is Addictive

Green again. Now that’s satisfying. Did you get that little dopamine rush when you reloaded the page?

With these changes in place, our test passes again. Our new code effectively compares the forward and backward string, and returns true when the string is the same forward and backward, and false otherwise.

This code is a pure function because it is just doing one thing, and doing it consistently given a consistent input value without creating any side effects, making any changes to variables outside of itself, or relying on the state of the application. Every time you pass this function a string, it does a comparison between the forward and backward string, and returns the result regardless of when or how it is called.

You can see how easy that kind of consistency makes this this function to unit test. In fact, writing test-driven code can encourage you to write pure functions because they are so much easier to test and modify.

And you want the satisfaction of a passing test. You know you do.

Refactoring a Pure Function

At this point it’s trivial to add additional functionality, such as handling non-string input, ignoring differences between uppercase and lowercase letters, etc. Just ask the product owner how they want the program to behave. Since we already have tests in place to verify that strings will be handled consistently, we can now add error checking or string coercion or whatever behavior we like for non-string values.

For example, let’s see what happens if we add a test for a number like 1001 that might be interpreted a palindrome if it were a string:

describe("isPalindrome", () => {
  it("returns true if the string is a palindrome", () => {
    expect(isPalindrome("abba")).toEqual(true);
  });
  it("returns false if the string isn't a palindrome", () => {
    expect(isPalindrome("Bubba")).toEqual(false);
  });
  it("returns true if a number is a palindrome", () => {
    expect(isPalindrome(1001)).toEqual(true);
  });
});

Doing this gives us a red screen and a failing test again because our current is isPalindrome function doesn’t know how to deal with non-string inputs.

Panic sets in. We see red. The test is failing.

But now we can safely update it to handle non-string inputs, coercing them into strings and checking them that way. We might come up with a function that looks a little bit more like this:

const isPalindrome = (str) => {
  return str
    .toString()
    .split("")
    .reverse()
    .join("") === str.toString();
};

And now all our tests pass, we’re seeing green, and that sweet, sweet dopamine is flooding into our our test-driven brains.

By adding toString() to the evaluation chain, we’re able to accommodate non-string inputs and convert them to strings before testing. And best of all, because our other tests are still being run every time, we can be confident that we haven’t broken the functionality we got before by adding this new capability to our pure function. Here’s what we end up with:

See the Pen A Beginner’s Guide to Testing Functional JavaScript by SitePoint (@SitePoint) on CodePen.

Play around with this test, and start writing some of your own, using Jasmine or any other testing library that suits you.

Once you incorporate testing into your code design workflow, and start writing pure functions to unit test, you may find it hard to go back to your old life. But you won’t ever want to.

This article was peer reviewed by Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!