Frontend Engineering/
Lesson

You ask AI to write unit tests for your shopping cart moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.. It produces a beautiful test file with 15 tests, all green. You feel great, until a user enters a quantity of -3 and your cart shows a negative total. None of those 15 tests checked for negative numbers. AI tested what the code does, not what it should do.

Unit tests are the foundation of any testing strategy, and they are also where AI assistance is most deceptive. The tests look comprehensive. The output is formatted perfectly. But the test cases themselves reflect the same blind spots as the code.

What is a unit testWhat is unit test?An automated check that verifies a single function works correctly in isolation, with all external dependencies replaced by fakes.?

A unit is the smallest testable piece of code, typically a single function, method, or component. A unit test verifies that this piece works correctly in isolation, without databases, APIs, or other modules.

function calculateDiscount(price, percentage) {
  if (price < 0) throw new Error('Price cannot be negative');
  if (percentage < 0 || percentage > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return price * (percentage / 100);
}

test('calculateDiscount() applies percentage correctly', () => {
  expect(calculateDiscount(100, 20)).toBe(20);
  expect(calculateDiscount(50, 10)).toBe(5);
  expect(calculateDiscount(0, 50)).toBe(0);
});

test('calculateDiscount() throws on invalid input', () => {
  expect(() => calculateDiscount(-10, 20)).toThrow('Price cannot be negative');
  expect(() => calculateDiscount(100, 150)).toThrow('Percentage must be between 0 and 100');
});

Each test focuses on one aspect: correct calculation or error handling. This separation makes failures easy to diagnose. When AI generates this function, it will usually generate the first test but skip the second, it does not think to test its own error handling.

02

The AAA pattern

Every well-structured test follows three steps:

  • Arrange: set up the test data and preconditions
  • Act: execute the code under test
  • Assert: verify the result matches expectations
test('shopping cart calculates total correctly', () => {
  // Arrange
  const cart = new ShoppingCart();
  const item1 = { price: 10, quantity: 2 };
  const item2 = { price: 5, quantity: 1 };

  // Act
  cart.add(item1);
  cart.add(item2);
  const total = cart.getTotal();

  // Assert
  expect(total).toBe(25);
});

When AI generates tests, it usually follows this structure. What it gets wrong is the content of each step, it arranges trivial data, acts on the happy path, and asserts shallow properties.

AI pitfall
AI-generated tests frequently use the most obvious, simple inputs: add(2, 3), multiply(4, 5), formatName("Alice"). These tests pass but never stress the function. Real bugs hide in edge cases: add(Number.MAX_SAFE_INTEGER, 1), multiply(-0, Infinity), formatName(""). Always supplement AI-generated tests with edge case inputs the AI did not consider.
03

Testing frameworks: Jest and Vitest

Jest is the most popular JavaScript testing framework. Vitest is the modern alternative, faster, native TypeScript support, and built for Vite projects.

import { describe, test, expect } from 'vitest';
import { add, subtract, multiply } from './math';

describe('Math utilities', () => {
  test('add() returns sum of two numbers', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  test('subtract() returns difference', () => {
    expect(subtract(5, 3)).toBe(2);
    expect(subtract(3, 5)).toBe(-2);
  });
});
FeatureJestVitest
SpeedModerateFast (native ESM)
TypeScriptNeeds ts-jest configBuilt-in support
ConfigurationHeavy, lots of optionsMinimal, uses Vite config
EcosystemLargest, most pluginsGrowing, Jest-compatible API
Best forExisting projects, Node.jsNew projects, Vite/React

Both use the same APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. (describe, test, expect), so switching between them is straightforward. When AI generates test files, check which framework it targets, mixing Jest globals with Vitest imports causes confusing failures.

04

Common assertions

Assertions are how you verify results. Here are the ones you will use most:

AssertionWhat it checksExample
toBe()Strict equality (===)expect(value).toBe(5)
toEqual()Deep equality (objects/arrays)expect(obj).toEqual({a: 1})
toBeTruthy()Truthy valueexpect(value).toBeTruthy()
toBeNull()Null valueexpect(value).toBeNull()
toContain()Array/string contains itemexpect(arr).toContain(item)
toThrow()Function throws errorexpect(fn).toThrow('msg')
toHaveLength()Array/string lengthexpect(arr).toHaveLength(3)
toBeCloseTo()Floating point near-equalityexpect(0.1 + 0.2).toBeCloseTo(0.3)
AI pitfall
When AI generates assertions, watch for toBeTruthy() where toBe(true) is needed. toBeTruthy() passes for any truthy value, the number 1, a non-empty string, an object, even an empty array []. If your function should return specifically true, use toBe(true). Shallow assertions are a major source of false-positive tests that pass when the code is broken.
05

Mocking dependencies

Unit tests should be isolated. If your function calls an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. or database, you mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions. (fake) that dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. so the test only exercises your logic.

import { vi } from 'vitest';

// Mock the API module
vi.mock('./api', () => ({
  fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'Alice' }))
}));

test('loadUserProfile formats display name', async () => {
  const profile = await loadUserProfile(1);
  expect(profile.displayName).toBe('Alice');
});

Mocking lets you control the environment and test scenarios that are hard to reproduce, network failures, timeout errors, empty responses.

AI pitfall
AI loves to mock everything. When you ask it to write unit tests, it often mocks the very logic you are trying to test. If your function processOrder() calls calculateTotal() and applyDiscount(), and AI mocks both of those, your test verifies nothing, it only confirms that your function calls two other functions. Mock external I/O (APIs, databases, file system). Do not mock internal business logic.
06

Testing async code

Modern JavaScript is full of async operations. Tests handle them with async/await:

test('fetches user data', async () => {
  const data = await fetchUser(1);
  expect(data.name).toBe('Alice');
});

test('handles missing user', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

Always await the assertionWhat is assertion?A statement in a test that checks whether a value matches what you expected, failing the test if it does not. when testing rejected promises. Forgetting await makes the test pass regardless of what the promiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits. does, the test finishes before the promise settles. When AI generates async tests, it sometimes forgets the await on rejects.toThrow(), creating a test that never fails.

07

What AI-generated unit tests typically miss

Here is a pattern you will see repeatedly. AI writes this test suite for a validateEmail function:

// AI-generated tests (looks complete, is not)
test('accepts valid email', () => {
  expect(validateEmail('user@example.com')).toBe(true);
});

test('rejects email without @', () => {
  expect(validateEmail('userexample.com')).toBe(false);
});

test('rejects empty string', () => {
  expect(validateEmail('')).toBe(false);
});

Three tests, all pass. But what about these cases?

// Tests AI should have written
test('rejects email with spaces', () => {
  expect(validateEmail('user @example.com')).toBe(false);
});

test('handles null input', () => {
  expect(validateEmail(null)).toBe(false);
});

test('handles undefined input', () => {
  expect(validateEmail(undefined)).toBe(false);
});

test('rejects double dots in domain', () => {
  expect(validateEmail('user@example..com')).toBe(false);
});

test('handles extremely long input', () => {
  const longEmail = 'a'.repeat(1000) + '@example.com';
  expect(validateEmail(longEmail)).toBe(false);
});

The AI tested the obvious cases. The real bugs live in the non-obvious ones. This is not a one-off problem, it is the pattern with every AI-generated test suite.

08

Unit testing decision table

When reviewing AI-generated tests or writing your own, use this table to decide what to test:

Input categoryExamplesAI tests this?You must test this
Happy pathValid email, positive numberAlmost alwaysYes, as a baseline
Empty input"", [], {}, null, undefinedRarelyAlways
Boundary values0, -1, MAX_SAFE_INTEGERRarelyAlways for numeric functions
Type mismatchesString where number expectedAlmost neverWhen function is public-facing
Special charactersUnicode, emoji, HTML injectionAlmost neverFor user-facing inputs
Concurrent/async edge casesRace conditions, timeoutsNeverFor async functions
09

Unit testing best practices

DoDo not
Test one concept per testCombine multiple assertions about different behaviors
Use descriptive test namesName tests vaguely ('test user')
Test behavior (return values, side effects)Test implementation details (private state)
Mock external I/O onlyMock internal logic you are trying to test
Keep tests independentShare mutable state between tests
Test edge cases explicitlyTrust that happy-path tests are sufficient

Unit tests are your first line of defense. They catch logic errors early, document expected behavior, and give you confidence to refactor AI-generated code. But only if you write them to challenge the code, not confirm it.