Learning To Write Tests

In my previous blog entry, I talked about why testing is important. Ideally we want to prove to ourselves (and our customers) that we are confident our code solves an “edge case” or some acceptance criteria (AC for short). Testing is a good way to “cover yourself”, especially if you work at large companies and have to show multiple people/customers your work and they can quickly just use a computer to verify.

It’s important to know the theory as to why to test, but you also really need to “do the work” to really let it soak in.

Let’s Dive In To Testing

My framework of choice for testing is Jest. It’s a pretty good library, and definitely one any JavaScript Full Stack Developer should know. Setup is pretty straightforward if you are used to the standard “npm workflow”. (Note, if you don’t use JavaScript, still try to just read along and get a feel for what’s happening. If you get the gist, you can look at testing libraries for your language, e.g. Python, Java, etc).

$ mkdir myTests && cd $_
$ npm init -y
$ npm install jest

Great you’ve got Jest installed! Now it’s time to write up a simple test. Let’s make a file called practice.test.js. You can either do that in the terminal or open up your favorite code editor and create the file. In both cases, be sure to open your code editor to this project folder so we can work on the practice.test.js file. Add in this code snippet:

const sum = (num1, num2) => num1 + num2;

test("sum function sums 2 two numbers", () => {
  expect(sum(1, 2)).toBe(3);
});

We’re basically writing out a test that takes two arguments, a string we’ll use to label this test, e.g. “sum function sums two numbers”, and a callback function to execute that checks our code actually works. We expect that calling sum on 1 and 2 to be 3. (We could change the inputs here and expect any other combination of two numbers to return any sum we expect; the point here is to test that the function on line 1, sum, actually sums two numbers and returns a result.

Go ahead and update your package.json file in the scripts area so that we can run Jest easily from the command line. Update the config to look like:

{ 
"name": "mytests",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
 ...
}

What we’re saying here is that whenever we run the command npm test in our console, we want Jest to kick in and look at our project folder. Jest will look for any files that sit inside a test folder or a spec folder, or also have a filename pattern like *.test.js, where * can be any file name.

Run the Test

Here comes the fun part, seeing the result of our hard work. Enter npm test in your console and watch Jest kick into gear. If all goes well, you should see output like this:

PASS  ./practice.test.js
  ✓ sum function sums 2 two numbers

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.143 s, estimated 1 s

Get Conceptual

Okay, pretty good so far. Let’s take a moment to think like a “business”. In the real world, you get directions from someone, often your boss, but also often project managers and other team members. They might tell you — “Hey, we really want to build a cool function for our website. Do you think you can make it happen? Here’s the design, the user story and AC.”

You might get a ticket like this (where the story is outlined for you):

===

Ticket #431

AC: “As a user, I should be able to input two numbers and see a sum”

Expect: “See the sum in my console.”

AC: “As a user, I should see an error if I’ve not entered the required amount of inputs for my sum function”.

Expect: “See an error if 0 or 1 inputs passed to sum function”. AC: “As a user, I expect that if I pass letters, or unknown characters to sum function, it should show an error letting me know I’ve entered wrong inputs.”

Expect: “See an error if I pass unknown inputs to sum function”.

====

I like tickets where AC and Expect statements are clearly outlined, because it makes it very simple to take this “Story” and translate it over to Jest. (Note: this isn’t always the case. But for tutorial purposes, I think this helps a lot for anyone who might be writing code and wondering how exactly to start testing their work. Even if ACs/Stories aren’t written out for you like so, you can always take a step back and write a basic outline describing what your code should do, and what you expect in simple statements).

Expect statements can map over to our expect blocks of code, while ACs can map over to the string we need to label our test.

Since we’ve already tested the first AC, “…input two numbers and see a sum”, we can be confident that we’ve met the Expect block for that criteria. We only need to make sure we now address the remaining ACs (two remaining) in our story to finish this ticket. Then we can feel confident about delivering our work.

Break the Test

Let’s now break the test we wrote. Remove the first parameter for your sum function like so:

test("sum function sums 2 two numbers", () => {
  expect(sum(2)).toBe(3);
});

What should happen? What do you expect? When you’re ready, run npm test.

The Test Fails! (No Surprise Here, Really)

You should see output like:

FAIL  ./practice.test.js
  ● Test suite failed to run

    Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.

      5 | });
      6 |
    > 7 | test(" ");
        | ^
      8 |

      at Object.<anonymous> (practice.test.js:7:1)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.148 s, estimated 1 s

So what do we do about this failing test? Well according to our “AC”, we should expect to see a message that tells us we made an error with our input. Let’s fix our code so we can display this message if the wrong number of inputs are entered. Update your code to look like:

const requiredInputError =
  "Sorry you did not enter the required amount of inputs";
const sum = (num1, num2) => {
  if (!num1 && !num2) {
    return requiredInputError;
  }
  return num1 + num2;
};

test("sum function sums 2 two numbers", () => {
  expect(sum(1, 2)).toBe(3);
});

test("sum function shows an error message if inputs are less than 2", () => {
  expect(sum()).toBe(requiredInputError);
});

Run npm test again in your console. You should see tests pass.

What we’ve done here is create one more test where we try to call the sum function without any inputs. Our test expects that if we don’t call with inputs less than 2, we’ll see a requiredInputError message instead. (If you feel good so far, improve this test on your own to better safeguard against the wrong amount of inputs).

Last Criteria

Okay, we’ve made it. Let’s now finish the last block of our Story, “As a user, I expect that if I pass letters, or unknown characters to sum function, it should show an error letting me know I’ve entered wrong inputs.”

Basically we want to safeguard against non-numerical inputs. (Again, feel free to improve the code as you like if you get the gist). Update your code to:

const requiredInputError =
  "Sorry you did not enter the required amount of inputs";
const validInputError = "Sorry you did not enter a valid input";
const sum = (num1, num2) => {
  if (!num1 && !num2) {
    return requiredInputError;
  }
  if (typeof num1 !== "number" || typeof num2 !== "number") {
    return validInputError;
  }
  return num1 + num2;
};

test("sum function sums 2 two numbers", () => {
  expect(sum(1, 2)).toBe(3);
});

test("sum function shows an error message if inputs are less than 2", () => {
  expect(sum()).toBe(requiredInputError);
});

test("sum function shows an error message if inputs are not numbers", () => {
  expect(sum("l", "b")).toBe(validInputError);
});

Run npm test again and you should see all tests passing. (If not, please try to go back and figure out what you might have mistyped or overlooked). At this point, all three of our ACs are good to go. You can now send this code up for review from your peers or manager — who might also give you even more feedback to make this code REALLY good.

You should also try breaking the tests to understand why things might fail. That helps you learn faster about how to fix things if later you run into errors later on other projects. Go ahead and try to remove one of the if blocks and try re-running npm test. Does the code still work, or does it break? Can you figure out what the error message from Jest is in the console?

Next Up

What I really like about Jest is that there is very little config, if any. It just works. It’s really easy to just start writing a simple test in a simple sandbox as I’ve demonstrated above, and then build up. We can do more of those kind of exercises in future blog posts, but I was thinking it might be helpful to look at something more complex — the kind of tests you’d see in a large scale web app. That way you know what you’ll need to build your Jest and testing skills up to, so you can also work on large scale apps as well.

For my next example, I’ll be working with code from HackBuddy’s Team Hack Storybook repository. (Check out the repo here, and let us know if you’re interested in helping us out). We’re always looking for developers who want to level up their skills with us.

Stay tuned: Testing A Larger Scale App with Jest, React & TypeScript