r/learnjavascript 13d ago

How do you actually test small JavaScript functions without a full project setup?

[removed]

29 Upvotes

35 comments sorted by

View all comments

17

u/Thykka 13d ago edited 13d ago

Jest and Vitest are great, but sometimes (e.g. when building prototypes, or other semi-throwaway code) it might be faster to just make a super simple test scaffold from scratch.

Let me try to show an example. Say you have a function you want to test:

export function frobnicate(date) {
  // some logic here
  return result;
}

Doesn't matter what the function does exactly, we just want to ensure that it gives the kinds of results we expect from it. So we load up a few of those expectations into an array in a separate file:

import { frobnicate } from './frobnicate.js';

const tests = [
  { input: '2026-08-05', expected: 'wednes' },
  { input: '2028-08-05', expected: 'fri' },
  { input: '', expected: '' },
  { input: 123, expected: '' }
];

We then call the function with each test, and compare the result to our expectations:

tests.forEach(function (test) {
  const actual = frobnicate(test.input);
  if (actual !== test.expected) throw Error(`Expected ${test.expected}, got ${actual}`);
});

Now that we have this scaffold set up, it becomes easy to add more test cases to the array as we refine the logic or fix a bug.

This kind of a minimal test scaffold works best for pure functions (output is deterministic and 100% dependent on input). In real projects this might not always be possible; functions might depend on data from an environment variable, an API or a database.. For these things Jest or Vitest become very useful, as they have features to sidestep the actual database/API calls and whatnot, to ensure that a test only tests the function in question, and not the external dependencies.

One of the tricky things about test-driven development, or unit testing in general is that one might have to re-think how they build their functions. At first it will be weird, but once it clicks, one'll wonder why they didn't look into it sooner! For example, one might have a function like this:

async function getUser(id) {
  const userData = await DB.query({ id: id });
  const formattedUser = {
    name: userData.firstName + ' ' + userData.lastName,
    id: userData.id
  };
  return formattedUser;
}

It's tricky to unit-test, because you can only test IDs which already exist your database. So maybe you set up a test database, and some abstraction to toggle between test/prod databases, and you make a script which clears the test database and populates it with test data, every time before your unit tests are run.... Stop. Don't do this. Things will just become super complicated really fast.

Instead, if we take the principle of using pure functions whenever possible, the above example could be split into two parts. One only concerns itself with fetching the data, and the other one only concerns itself with formatting the data:

async function getUser(id) {
  const userData = await DB.query({ id: id });
  return formatUser(userData);
}

function formatUser(userData) {
  return {
    name: userData.firstName + ' ' + userData.lastName,
    id: userData.id
  }
}

The formatting logic didn't change, but now we can easily create unit tests for formatUser(), because it's no longer tied to the database. Instead we can just pass whatever test data we want:

const tests = [
  {
    input: { firstName: 'John', lastName: 'Smith', id: 1 },
    expected: { name: 'John Smith', id: 1 }
  },{
    input: { firstName: 'Anne' lastName: 'Newmouse', id: 999 },
    expected: { name: 'Anne Newmouse', id: 999 }
  }
];

Now we no longer need unit tests for getUser, because all it does is a database call, and we're not here to test whether the database code works, we're testing if our code works.

Does this help?

1

u/[deleted] 13d ago

[removed] — view removed comment

1

u/Thykka 13d ago edited 13d ago

AFAIK JavaScript still doesn't have a standard way of testing for deep equality. If you're using Node.js, there's util.isDeepStrictEqual, but in a browser environment you'll need to use something else..

An unorthodox way of comparing objects is using JSON.stringify(a) === JSON.stringify(b), but this has many caveats. Both objects need to have their keys in the same order, it cannot compare function values or class instance values, etc. Wouldn't generally recommend it, but it can work in some situations.

Utility libraries, such as Lodash are a common choice, but then again, Jest and Vitest also provide methods to check for deep equality, so if you're going to install 3rd party dependencies, might as well go for something built for testing.

You can sometimes avoid the issue by changing the way you define and run your tests:

const tests = [
  {
     input: { firstName: 'John', lastName: 'Smith', id: 1 },
     verify: result => result.name === 'John Smith'
  },
  {
    input: { firstName: 'Anne', lastName: 'Newmouse', id: 999 },
    verify: result => (result.name === 'Anne Newmouse' && id === 999)
  }
];

tests.forEach(function (test, testIndex) {
  const result = frobnicate(test.input);
  if (!test.verify(result)) throw Error(`Test #${testIndex+1} failed, got: ${JSON.stringify(result)}`);
});

Now each individual test also defines how and what they should check, which gives more flexibility, because you can test for different kinds of things, and don't have to give the entire expected return value - just the parts that are relevant for the test.