CodingNic

Testing Fundamentals with Jest

Testing Functions, Including Error Cases

Testing Fundamentals with Jest 20 min read

Testing Functions, Including Error Cases

Objectives

By the end of this lesson, you should be able to:

  • Test that a function throws when it should, with toThrow
  • Test an async function’s resolved value with resolves
  • Write tests covering both the success case and the failure case for the same function

💡 Why this matters: A function that’s only tested for its success case is only half tested, this course has relied on functions throwing (Module 5’s Zod schemas) and rejecting (every bcrypt.compare call) correctly, this lesson tests exactly that.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

Testing a Function That Throws

javascript
// password.js
function checkPasswordStrength(password) {
  if (typeof password !== 'string' || password.length < 8) {
    throw new Error('Password must be at least 8 characters');
  }
  return true;
}

module.exports = { checkPasswordStrength };
javascript
// password.test.js
const { checkPasswordStrength } = require('./password');

describe('checkPasswordStrength', () => {
  it('returns true for a strong enough password', () => {
    expect(checkPasswordStrength('sunshine123')).toBe(true);
  });

  it('throws for a password that is too short', () => {
    expect(() => checkPasswordStrength('short')).toThrow('Password must be at least 8 characters');
  });

  it('throws for a non-string password', () => {
    expect(() => checkPasswordStrength(12345678)).toThrow();
  });
});

expect(() => checkPasswordStrength('short')).toThrow('...') wraps the call in an arrow function, calling checkPasswordStrength('short') directly, outside of expect, would throw immediately and crash the test itself, wrapping it in a function lets Jest call it and catch the throw. Passing a string to toThrow(...) checks the error message contains that text, calling toThrow() with no argument just checks that something was thrown, without checking the message.

Running Both Success and Failure Cases

text
PASS jesttest/password.test.js
  checkPasswordStrength
    ✓ returns true for a strong enough password
    ✓ throws for a password that is too short (4 ms)
    ✓ throws for a non-string password (1 ms)

Three tests, one confirming the expected success case, two confirming distinct ways the function is supposed to reject bad input, together they describe the function’s full contract, not just its happy path.

Testing an Async Function

javascript
// hashing.js
const bcrypt = require('bcryptjs');

async function hashPassword(password) {
  return bcrypt.hash(password, 10);
}

async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash);
}

module.exports = { hashPassword, verifyPassword };
javascript
// hashing.test.js
const { hashPassword, verifyPassword } = require('./hashing');

describe('hashing', () => {
  it('hashes a password to something other than the original', async () => {
    const hash = await hashPassword('sunshine123');
    expect(hash).not.toBe('sunshine123');
  });

  it('verifies a correct password against its hash', async () => {
    const hash = await hashPassword('sunshine123');
    await expect(verifyPassword('sunshine123', hash)).resolves.toBe(true);
  });

  it('rejects an incorrect password against the hash', async () => {
    const hash = await hashPassword('sunshine123');
    await expect(verifyPassword('wrong-password', hash)).resolves.toBe(false);
  });
});

Each it callback is async, letting await be used directly inside it, exactly like any other async code in this course. await expect(promise).resolves.toBe(value) awaits the promise, then asserts on what it resolved to, the async equivalent of expect(value).toBe(...). There’s a matching .rejects for a promise that’s expected to reject, useful for testing async functions that throw, jsonwebtoken’s verify (Module 3) is a common case for that pattern.

Running the Async Tests

text
PASS jesttest/hashing.test.js
  hashing
    ✓ hashes a password to something other than the original (110 ms)
    ✓ verifies a correct password against its hash (140 ms)
    ✓ rejects an incorrect password against the hash (140 ms)

Three tests, again covering both the success case, a correct password verifies, and two meaningfully different outcomes, a hash isn’t the plain password (Module 1), and a wrong password is correctly rejected.

Try It

  1. Write checkPasswordStrength and its three tests, and confirm all three pass.
  2. Write hashPassword and verifyPassword and their three tests, using bcryptjs, and confirm all three pass.
  3. Add a test for checkPasswordStrength(undefined), and confirm it throws, without needing to know the exact message.
  4. Pick one function from an earlier module in this course (for example, Module 5’s Zod schema validation, or Module 3’s jwt.verify), and write at least one success-case test and one failure-case test for it.

Recap

  • expect(() => fn()).toThrow(message) tests that a function throws, wrapping the call in an arrow function is required.
  • await expect(promise).resolves.toBe(value) tests an async function’s resolved value, .rejects is the equivalent for an expected rejection.
  • A well-tested function has tests for its success case and its meaningfully distinct failure cases, not just the happy path.

Next lesson: mocking a dependency, so a unit test doesn’t need a real database or network call to run.