LearnThatStack Ace your next interview
Testing · Free

Unit Testing.
Interview cheat sheet.

Quick reference for Unit Testing - sectioned for fast scanning. Skim the part you're shaky on, walk in confident.

Testing 13-section reference ~7 min read

Summary

Unit testing validates individual components in isolation, forming the foundation of reliable software. Master test structure (AAA pattern), FIRST principles, mocking strategies, TDD practices, and framework-specific features to write fast, maintainable tests that catch bugs early and enable confident refactoring.

Fundamentals

What is Unit Testing?

  • Definition: Testing individual units/components in isolation
  • Unit: Smallest testable part (function, method, class)
  • Goal: Verify each unit works correctly independently

Why Unit Testing?

  1. Early Bug Detection: Catch issues during development
  2. Documentation: Tests serve as living documentation
  3. Refactoring Safety: Confidence when changing code
  4. Design Improvement: Forces modular, testable code
  5. Regression Prevention: Ensure new changes don't break existing functionality

Key Concepts

Test Structure (AAA Pattern)

test('should calculate total price', () => {
  // Arrange
  const items = [{ price: 10 }, { price: 20 }];
  
  // Act
  const total = calculateTotal(items);
  
  // Assert
  expect(total).toBe(30);
});

F.I.R.S.T Principles

  • Fast: Tests run quickly
  • Independent: No dependencies between tests
  • Repeatable: Same results every time
  • Self-validating: Pass/fail without manual check
  • Timely: Written just before production code

Test Coverage

  • Line Coverage: % of code lines executed
  • Branch Coverage: % of decision branches tested
  • Function Coverage: % of functions called
  • Statement Coverage: % of statements executed

Note: 100% coverage ≠ bug-free code

Types of Assertions

// Equality
expect(result).toBe(5);           // Strict equality
expect(result).toEqual({a: 1});   // Deep equality

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3);

// Strings
expect(text).toMatch(/pattern/);

// Arrays/Iterables
expect(array).toContain(item);
expect(array).toHaveLength(3);

// Exceptions
expect(() => throwError()).toThrow();

Testing Patterns

Test Doubles

  1. Dummy: Passed but never used
  2. Stub: Provides preset answers
  3. Spy: Records information about calls
  4. Mock: Pre-programmed with expectations
  5. Fake: Working implementation, simplified

Example: Mock vs Stub

// Stub - returns canned response
const stub = jest.fn().mockReturnValue(42);

// Mock - verifies behavior
const mock = jest.fn();
callFunction(mock);
expect(mock).toHaveBeenCalledWith('arg');

Mocking & Stubbing

Basic Mocking

// Mock a module
jest.mock('./api');

// Mock implementation
const mockFn = jest.fn();
mockFn.mockImplementation((x) => x * 2);

// Mock return values
mockFn.mockReturnValue(10);
mockFn.mockReturnValueOnce(5);

// Mock promises
mockFn.mockResolvedValue(data);
mockFn.mockRejectedValue(error);

Spying

// Spy on existing method
const spy = jest.spyOn(obj, 'method');

// Verify calls
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith(arg1, arg2);

// Restore original
spy.mockRestore();

Test-Driven Development (TDD)

Red-Green-Refactor Cycle

  1. Red: Write failing test
  2. Green: Write minimal code to pass
  3. Refactor: Improve code, keep tests green

Example TDD Flow

// 1. Red - Write test first
test('should validate email', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidEmail('invalid')).toBe(false);
});

// 2. Green - Implement minimal solution
function isValidEmail(email) {
  return email.includes('@');
}

// 3. Refactor - Improve implementation
function isValidEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

Best Practices

1. Write Descriptive Test Names

// ❌ Bad
test('test1', () => {});

// ✅ Good
test('should return user name when valid ID provided', () => {});

2. One Assertion Per Test (When Possible)

// ❌ Bad - Multiple unrelated assertions
test('user operations', () => {
  expect(createUser(data)).toBeTruthy();
  expect(deleteUser(id)).toBe(true);
  expect(updateUser(id, data)).toEqual(data);
});

// ✅ Good - Focused tests
test('should create user successfully', () => {
  expect(createUser(data)).toBeTruthy();
});

3. Use beforeEach/afterEach for Setup/Teardown

describe('UserService', () => {
  let service;
  
  beforeEach(() => {
    service = new UserService();
  });
  
  afterEach(() => {
    jest.clearAllMocks();
  });
  
  test('should fetch user', () => {
    // Test implementation
  });
});

4. Test Edge Cases

describe('divide function', () => {
  test('should divide positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });
  
  test('should handle division by zero', () => {
    expect(() => divide(10, 0)).toThrow();
  });
  
  test('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });
});

5. Keep Tests DRY with Helper Functions

// Helper function
function createTestUser(overrides = {}) {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}

// Usage in tests
test('should update user email', () => {
  const user = createTestUser({ email: 'new@example.com' });
  // Test implementation
});

Common Anti-Patterns

1. Testing Implementation Details

// ❌ Bad - Tests internal implementation
test('should call internal method', () => {
  const spy = jest.spyOn(obj, '_privateMethod');
  obj.publicMethod();
  expect(spy).toHaveBeenCalled();
});

// ✅ Good - Tests behavior/output
test('should return processed data', () => {
  const result = obj.publicMethod();
  expect(result).toEqual(expectedOutput);
});

2. Excessive Mocking

// ❌ Bad - Mocking everything
jest.mock('./userService');
jest.mock('./emailService');
jest.mock('./logger');
jest.mock('./validator');

// ✅ Good - Mock only external dependencies
jest.mock('./api'); // External API only

3. Non-Deterministic Tests

// ❌ Bad - Depends on current time
test('should create timestamp', () => {
  const result = createTimestamp();
  expect(result).toBe(Date.now()); // Flaky!
});

// ✅ Good - Control time
test('should create timestamp', () => {
  const now = 1234567890;
  jest.spyOn(Date, 'now').mockReturnValue(now);
  expect(createTimestamp()).toBe(now);
});

Testing Frameworks

JavaScript/Node.js

  • Jest: Most popular, zero config
  • Mocha: Flexible, requires assertion library
  • Vitest: Fast, Vite-native
  • Jasmine: Behavior-driven development

Other Languages

  • Java: JUnit, TestNG
  • Python: pytest, unittest
  • C#: NUnit, xUnit, MSTest
  • Ruby: RSpec, Minitest
  • Go: Built-in testing package

Advanced Topics

Parameterized Tests

// Jest
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 2, 4],
])('add(%i, %i) returns %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

Async Testing

// Promises
test('should fetch data', async () => {
  const data = await fetchData();
  expect(data).toEqual({ id: 1 });
});

// Callbacks
test('should call callback', (done) => {
  fetchData((data) => {
    expect(data).toEqual({ id: 1 });
    done();
  });
});

Snapshot Testing

test('should render correctly', () => {
  const component = render(<Button />);
  expect(component).toMatchSnapshot();
});

Code Coverage Configuration

// jest.config.js
{
  "collectCoverage": true,
  "coverageThreshold": {
    "global": {
      "branches": 80,
      "functions": 80,
      "lines": 80,
      "statements": 80
    }
  }
}

Critical Interview Topics

Test Level Distinctions

  • Unit Tests: Test single functions/methods in isolation, mock dependencies, milliseconds execution
  • Integration Tests: Test component interactions, use real dependencies where possible, seconds execution
  • E2E Tests: Test complete user workflows, full stack involved, minutes execution
  • Test Pyramid: 70% unit, 20% integration, 10% E2E for optimal balance

Mocking Strategy Decisions

When to Mock

  • External Services: APIs, databases, file systems
  • Slow Operations: Network calls, heavy computations
  • Non-Deterministic: Random values, timestamps, external state
  • Complex Setup: Multi-step initialization, expensive resources

When NOT to Mock

  • Simple Value Objects: DTOs, models, POJOs
  • Pure Functions: Mathematical operations, utilities
  • Core Business Logic: The actual code under test
  • Fast Operations: In-memory calculations, simple transformations

Testing Private Methods

  • Best Practice: Test through public API that uses private methods
  • Refactoring Signal: Complex private methods may need extraction
  • Design Principle: If it's worth testing, it's worth making public
  • Alternative: Move to separate testable utility class

Characteristics of Quality Tests

  • Fast Execution: Milliseconds per test
  • Isolated: No shared state, order independence
  • Repeatable: Same result every run
  • Self-Validating: Clear pass/fail, no manual verification
  • Timely: Written with or before production code
  • Single Concept: One logical assertion per test
  • Clear Failure: Obvious what broke from test name/message

Common Testing Scenarios

  1. How would you test a function that makes API calls?
// Mock the API module
jest.mock('./api');
test('should fetch user data', async () => {
  api.getUser.mockResolvedValue({ id: 1, name: 'John' });
  const user = await fetchUserData(1);
  expect(user.name).toBe('John');
});
  1. How do you test error handling?
test('should handle API errors', async () => {
  api.getUser.mockRejectedValue(new Error('Network error'));
  await expect(fetchUserData(1)).rejects.toThrow('Network error');
});
  1. How would you test time-dependent code?
beforeEach(() => {
  jest.useFakeTimers();
});

test('should timeout after 5 seconds', () => {
  const callback = jest.fn();
  startTimer(callback, 5000);
  
  jest.advanceTimersByTime(4999);
  expect(callback).not.toHaveBeenCalled();
  
  jest.advanceTimersByTime(1);
  expect(callback).toHaveBeenCalled();
});

Quick Reference

Essential Jest Matchers

.toBe()              // Strict equality (===)
.toEqual()           // Deep equality
.toBeNull()          // Checks for null
.toBeUndefined()     // Checks for undefined
.toBeDefined()       // Opposite of undefined
.toBeTruthy()        // Truthy values
.toBeFalsy()         // Falsy values
.toContain()         // Array/string contains
.toThrow()           // Function throws error
.toHaveBeenCalled()  // Mock function called

Test Organization

describe('Calculator', () => {
  describe('add method', () => {
    it('should add positive numbers', () => {});
    it('should add negative numbers', () => {});
  });
  
  describe('subtract method', () => {
    it('should subtract numbers', () => {});
  });
});

Mock Utilities

jest.fn()                      // Create mock function
jest.mock()                    // Mock entire module
jest.spyOn()                   // Spy on method
.mockReturnValue()             // Set return value
.mockImplementation()          // Custom implementation
.mockResolvedValue()           // Async success
.mockRejectedValue()           // Async failure

Interview Success Strategies

Technical Demonstration

  • Start Simple: Begin with happy path, then add edge cases
  • Edge Case Coverage: Null, undefined, empty arrays, zero, negative numbers, boundaries
  • Error Scenarios: Invalid inputs, exceptions, timeout handling
  • Code Organization: Clear test structure, descriptive names, helper functions

Strategic Thinking

  • Coverage Balance: Explain why 100% coverage isn't always optimal
  • Maintenance Trade-offs: Simple tests vs DRY principle
  • Test Speed: Fast feedback loop importance
  • Documentation Value: Tests as living documentation

Best Practices to Showcase

  • AAA Pattern: Clear Arrange-Act-Assert structure
  • Descriptive Naming: Test names describe behavior, not implementation
  • Single Responsibility: One concept per test
  • Test Independence: No shared state or order dependencies
  • Deterministic: No random values or external dependencies

Communication Points

  • TDD Benefits: Better design, built-in regression suite, confidence in refactoring
  • Mock vs Stub vs Spy: Understand distinctions and appropriate usage
  • Testing Philosophy: Behavior over implementation
  • Team Practices: Code review includes tests, continuous integration, pair programming

Red Flags to Avoid

  • Testing implementation details instead of behavior
  • Overly complex test setup
  • Ignored or commented-out tests
  • Tests that sometimes pass, sometimes fail
  • No assertions in test methods
Found this useful? Pass it on.
Pro · $10/mo

The sheet is free. Pro goes deeper.

Pro opens the full question library behind every sheet, every refresher and a monthly AI allowance. One subscription, all formats.

Full question library All refreshers Cancel anytime