Unit Tests for Random Functions

Shotgun PostAmp Episode 3

Eric Elliott
Nov 7 · 5 min read
Photo: Brandon Bailey — Midnight Cowboy (CC-BY-2.0)

Shotgun is video series that lets you ride shotgun with me while I tackle real programming challenges for real apps and libraries. The videos are only available to members of EricElliottJS.com, but I’m journaling the adventures here.

One of the challenges you’ll face when you use TDD long enough is the question: “How do I test random functions?”

In this episode of the Shotgun series, we encountered this issue, because we want to generate a randomized schedule to send out automated social media updates.

It’s important to be careful with our time if we’re going to be productive, so it’s great that PostAmp allows us to batch create a bunch of social media posts at once, but we don’t want to dump 30 tweets on our followers in a great big batch once per week. People would quickly unfollow because you’re blowing up their notifications.

Instead, wouldn’t it be nice if we could batch them all up, and PostAmp could generate a randomized schedule to post them for us throughout the week?

But to do that, we’re going to need to write some functions that are going to produce random output.

Pure functions are the easiest kinds of functions to unit test. Just call them, and then assert that the output is what you expected it to be.

But what about functions with random output? We can’t assert that the output is what we expect it to be. Instead, we’ll need to assert that the output obeys some constraints.

The first step in the process of generating our random post schedule, we’re going to need to break the problem down. Let’s start by generating a random number inside a given range. The signature will look like this:

randomNumber = (start: Number, end: Number) => Number

Our constraints will be pretty simple:

  • Given no start and end, generate a random number greater than or equal to start
  • Given start and end, generate a random number less than or equal to end

When we’re dealing with random numbers, we want to be sure not just that a single number conforms to our constraints. We want a fair degree of certainty that all numbers generated will conform to our constraints.

To do that, I usually generate a big batch of them and test them all. Here’s what I came up with:

import { describe } from 'riteway';
import { randomNumber } from './utils.js';
describe('randomNumber', async assert => {
const start = 3;
const end = 20;
const numbers = Array.from({ length: 100 }, () => randomNumber(start, end));
assert({
given: 'start, end',
should: 'generate a random number greater than or equal to start',
actual: numbers.every(n => n >= start),
expected: true
});
assert({
given: 'start, end',
should: 'generate a random number less than or equal to end',
actual: numbers.every(n => n <= end),
expected: true
});
});

First, Write ONE Test, Make it Pass

If you watch the video, you’ll probably notice a couple things. First, my test descriptions were wrong. I said, “given no arguments” — I’ve corrected that in this blog post. Oops.

But you’ll also notice that I wrote one test at a time. I started with:

assert({
given: 'start, end',
should: 'generate a random number greater than or equal to start',
actual: numbers.every(n => n >= start),
expected: true
});

Before moving on to the next test, I watched this one fail, then wrote the implementation:

export const randomNumber = (start, end) =>
Math.round(Math.random() * (end - start) + start);

Then I wrote the next test:

assert({
given: 'start, end',
should: 'generate a random number less than or equal to end',
actual: numbers.every(n => n <= end),
expected: true
});

The next test passed already because I wrote the whole implementation. Sometimes, if I know what I want the implementation to look like, I’ll write it, then make a change to break the test, then watch the test fail, then fix the code and watch it pass again.

Why would I do all that? To test the tests!

If you haven’t seen a test fail, you don’t know if the test works.

If you watch the video, you’ll also notice that I made a silly mistake towards the end, when I tried to break the end test, I changed the code like this:

export const randomNumber = (start, end) =>
Math.round(Math.random() * (5 - start) + start);

Of course, that didn’t break the test, because 5 is still in the valid range. To try to hunt down that problem, I logged the actual array of outputs to the console. The wiser of you may have guessed that I did that on purpose for two reasons:

  1. I wanted to show that test debugging technique, and
  2. I wanted to show you that everybody makes mistakes.

Good guess! But that was a genuine mistake, and a particularly silly one. It was just a happy accident that we got those extra lessons. Remember, everybody makes mistakes. If we didn’t, we wouldn’t need TDD.

TDD acts like a double entry accounting ledger. For every change, you have two records of the change: In the code, and in your tests. If you can get both records to agree, you have double the confidence that you’ve written the code correctly.

Next Steps

Members can watch the new episode on EricElliottJS.com. If you’re not a member, now’s a great time to see what you’ve been missing!

Start your free lesson on EricElliottJS.com


Eric Elliott is the author of the books, “Composing Software” and “Programming JavaScript Applications”. As co-founder of EricElliottJS.com and DevAnywhere.io, he teaches developers essential software development skills. He builds and advises development teams for crypto projects, and has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He enjoys a remote lifestyle with the most beautiful woman in the world.

JavaScript Scene

JavaScript, software leadership, software development, and related technologies.

Eric Elliott

Written by

Make some magic. #JavaScript

JavaScript Scene

JavaScript, software leadership, software development, and related technologies.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade