Unit Testing Randomness

minefield-1

Let’s imagine hypothetically you were working on software that placed landmines in a minefield grid and you had a function that given the dimensions of a minefield, and a safe cell, you had to randomly place a certain number of landmines in the other cells of the grid. It looks something like this:

randomlyPlaceMines/index.js:

import {times, shuffle, take, isEqual} from 'lodash';

export default (configuration, row, column) => {
  const [row_count, column_count] = configuration.dimensions;
  const cells = [];

  times(row_count, (row_index) => {
    times(column_count, (column_index) => {
      if (!isEqual([row, column], [row_index, column_index])) {
        cells.push([row_index, column_index]);
      }
    });
  });

  return take(shuffle(cells), configuration.mine_count);
};

Since this function is designed to return a different minefield every time it is run – even with the exact same input – how would you write a unit test for this function?

Since we’re using Lodash’s shuffle function for our random selection, and JavaScript objects have prototypes which can be overridden, we can override the shuffle function temporarily during our test to ensure deterministic behaviour of our choosing.

For example: randomlyPlaceMines/index-spec.js:

import randomlyPlaceMines from '.';
import _ from 'lodash';

describe('randomlyPlaceMines', () => {
  const original_shuffle = _.shuffle;

  before(() => { _.shuffle = (a) => _.reverse(a); });

  after(() => { _.shuffle = original_shuffle; });

  it('should choose specified number of mines and exclude specified row and column', () => {
    const row = 0;
    const column = 0;
    const configuration = {dimensions: [5, 5], mine_count: 5};
    const mines = randomlyPlaceMines(configuration, row, column);
    assert.deepEqual(mines, [[4, 4], [4, 3], [4, 2], [4, 1], [4, 0]]);
  });
});

What we’re doing here is replacing the Lodash shuffle function with the Lodash reverse function, and so when our function is executed the mines are reversed instead of shuffled – which we can test.

Since this is overridden everywhere, we need to be careful to make sure we set it back to reverse after we’re done to avoid all sorts of unexpected behaviour!

This isn’t ideal so perhaps we could allow our function to accept a sorting mechanism so that we can override it safely by passing in a dependency for testing purposes. This is much more testable and looks like this:

randomlyPlaceMines/index.js:

import {times, shuffle, take, isEqual} from 'lodash';

export default (configuration, row, column, shuffleMethod = shuffle) => {
  const [row_count, column_count] = configuration.dimensions;
  const cells = [];

  times(row_count, (row_index) => {
    times(column_count, (column_index) => {
      if (!isEqual([row, column], [row_index, column_index])) {
        cells.push([row_index, column_index]);
      }
    });
  });

  return take(shuffleMethod(cells), configuration.mine_count);
};

So now there’s an additional parameter shuffleMethod which defaults to the Lodash shuffle method so we keep compatibility with existing usage.

This means that in our unit tests we can now pass in a different shuffle method of our choosing to create deterministic behaviour:

randomlyPlaceMines/index-spec.js:

import randomlyPlaceMines from '.';
import {reverse} from 'lodash';

describe('randomlyPlaceMines', () => {
  it('should choose specified number of mines and exclude specified row and column', () => {
    const row = 0;
    const column = 0;
    const configuration = {dimensions: [5, 5], mine_count: 5};
    const mines = randomlyPlaceMines(configuration, row, column, reverse);
    assert.deepEqual(mines, [[4, 4], [4, 3], [4, 2], [4, 1], [4, 0]]);
  });
});

Our unit tests are now a lot simpler and safer which we were able to do by modifying our function to accept a dependency as a parameter. This can be known as dependency injection.

Have you ever worked on a system that required randomness? How did you write automated tests for it?

Author: Alister Scott

Alister is an Excellence Wrangler for Automattic.

1 thought on “Unit Testing Randomness”

  1. Great insightful post. I would think in most cases of randomization, a random function is used and the random function could typically take a seed. So supplying the same fixed seed, would result in deterministic output. And that would be easy to do if one has access to seeding that random function.

    Liked by 1 person

Comments are closed.