Module 3 - Unit Testing

Unit Testing

Unit testing is a fundamental practice in software development that helps ensure code quality, catch bugs early, and make refactoring safer. In this module, you'll learn how to write effective unit tests using Jest, a popular JavaScript testing framework.

Learning Objectives

Testing Fundamentals

Understand the principles of unit testing, test-driven development (TDD), and the testing pyramid.

Jest Framework

Master Jest's features including test organization, assertions, mocks, and test coverage reporting.

Testing Patterns

Learn common testing patterns and best practices for writing maintainable and effective tests.

Module Content

Testing Basics

  • What is Unit Testing?
  • Test-Driven Development
  • Testing Pyramid

Jest Framework

  • Test Organization
  • Assertions & Matchers
  • Mocks & Spies

Testing Patterns

  • Test Structure
  • Edge Cases
  • Code Coverage

Testing Fundamentals

Why Do We Test?

Testing is an essential skill for a web developer to have. It's hard to anticipate every way that a user might interact with your site, not to mention it is incredibly time-consuming to test all of those options manually.

If we don't have tests, it's safe to assume the following:

  • Application code has to be tested manually
  • There is no way to know if a change broke another piece of code
  • You cannot be sure if the code is correct
  • Manually testing takes a lot of unnecessary time
  • Adding new features becomes slow

Advantages of Testing

  • Verifies edge cases
  • Developer can concentrate on current changes (safety net)

Drawbacks of Testing

  • More code to write and maintain
  • More tooling
  • Additional dependencies
  • May provide a false sense of security
  • Trivial test failures may break the build
  • Regressions (when a new feature breaks existing code)

Unit Testing with Jest

There are many testing tools available for JavaScript applications. Examples include Jest, Mocha/Chai, Jasmine, Qunit, Enzyme, Supertest, Istanbul, Karma, and Cypress.

For this module, we'll focus on Jest, a JavaScript testing framework that works particularly well with React applications.

What is Jest?

Jest is a test runner and command-line interface npm package. It was originally made by Facebook and is included out-of-the-box with create-react-app. Jest can run asynchronous tests, snapshot testing, and produce coverage reports.

Watch Mode

Instead of running tests manually, Jest has a built-in watch mode feature that will run tests automatically as files change. Jest detects these changes automatically and only runs the tests related to the changes.

Test-Driven Development

Test-Driven Development is a software development approach where tests are written before the code that needs to be tested. The TDD cycle is:

  1. Red: Write a failing test
  2. Green: Make the test pass with the minimum amount of code
  3. Refactor: Improve the code while keeping the tests passing

Benefits of TDD include:

  • Ensures testable code from the start
  • Forces you to think about edge cases early
  • Provides documentation of how your code should work
  • Creates a safety net for refactoring

Setting Up Jest for Testing

Configuring Jest

To set up Jest in a Node.js project, follow these steps:

1. Install Jest

npm install -D jest

2. Add Test Script to package.json

// In package.json
{
  "scripts": {
    "test": "jest --watch"
  }
}

3. Create Test Files

Jest will find your tests in two ways:

  • By placing .js files inside a folder called __tests__
  • By ending the name of a file in .test.js or .spec.js

4. Run Tests

npm test

Writing Basic Tests with Jest

Here's an example of a basic test file structure:

Code Example - Simple Math Functions and Tests:

// math.js
function sum(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { sum, multiply };

// math.test.js
const { sum, multiply } = require('./math');

describe('Math module', () => {
  // Test suite for the sum function
  describe('sum function', () => {
    test('adds 1 + 2 to equal 3', () => {
      expect(sum(1, 2)).toBe(3);
    });
    
    test('adds negative numbers correctly', () => {
      expect(sum(-1, -2)).toBe(-3);
    });
  });
  
  // Test suite for the multiply function
  describe('multiply function', () => {
    test('multiplies 2 * 3 to equal 6', () => {
      expect(multiply(2, 3)).toBe(6);
    });
    
    test('multiplies with zero to equal zero', () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });
});

Jest Test Organization

Jest provides several functions to organize your tests effectively:

Key Jest Functions:

  • describe(): Group related tests in a test suite
  • test() or it(): Define individual test cases
  • beforeEach(): Run code before each test in a suite
  • afterEach(): Run code after each test in a suite
  • beforeAll(): Run code once before all tests in a suite
  • afterAll(): Run code once after all tests in a suite

Code Example - Test Organization:

describe('User Authentication', () => {
  let db;
  
  // Setup - runs once before all tests
  beforeAll(() => {
    db = connectToTestDatabase();
  });
  
  // Cleanup - runs once after all tests
  afterAll(() => {
    disconnectFromDatabase(db);
  });
  
  // Reset for each test
  beforeEach(() => {
    resetTestUser(db);
  });
  
  // Test cases
  test('registers a new user successfully', () => {
    // Test registration
  });
  
  test('fails to register with invalid email', () => {
    // Test invalid registration
  });
  
  test('logs in an existing user', () => {
    // Test login
  });
});

Advanced Testing Techniques

Assertions and Matchers

Jest provides a rich set of matchers to test different types of values:

Common Matchers:

// Equality
expect(value).toBe(exactValue);            // Exact equality (===)
expect(value).toEqual(obj);                // Deep equality for objects

// Truthiness
expect(value).toBeTruthy();                // Truthy value
expect(value).toBeFalsy();                 // Falsy value
expect(value).toBeNull();                  // Null value
expect(value).toBeUndefined();             // Undefined value
expect(value).toBeDefined();               // Not undefined

// Numbers
expect(value).toBeGreaterThan(number);     // > number
expect(value).toBeGreaterThanOrEqual(number); // >= number
expect(value).toBeLessThan(number);        // < number
expect(value).toBeLessThanOrEqual(number); // <= number

// Strings
expect(string).toMatch(/regex/);           // Match regex

// Arrays and Iterables
expect(array).toContain(item);             // Contains item
expect(array).toHaveLength(number);        // Has length

// Objects
expect(object).toHaveProperty(keyPath);    // Has property

Mocks and Spies

Mocks allow you to replace real implementations with test doubles. This is particularly useful for:

  • Testing code that makes API calls
  • Isolating the code being tested from its dependencies
  • Verifying that functions were called with specific arguments

Code Example - Using Jest Mocks:

// userService.js
const axios = require('axios');

async function getUsers() {
  const response = await axios.get('https://api.example.com/users');
  return response.data;
}

module.exports = { getUsers };

// userService.test.js
const axios = require('axios');
const { getUsers } = require('./userService');

// Mock the axios module
jest.mock('axios');

test('fetches users successfully', async () => {
  // Setup mock response
  const users = [{ id: 1, name: 'John' }, { id: A, name: 'Jane' }];
  axios.get.mockResolvedValue({ data: users });
  
  // Call the function
  const result = await getUsers();
  
  // Verify axios.get was called correctly
  expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users');
  
  // Verify result
  expect(result).toEqual(users);
});

Testing Asynchronous Code

Jest provides several ways to test asynchronous code:

Code Example - Testing Async Functions:

// Using async/await (recommended)
test('async function test with async/await', async () => {
  const data = await fetchData();
  expect(data).toBe('expected data');
});

// Using promises
test('async function test with promises', () => {
  return fetchData().then(data => {
    expect(data).toBe('expected data');
  });
});

// Using the done callback (older style)
test('async function test with done callback', done => {
  fetchData().then(data => {
    expect(data).toBe('expected data');
    done();
  });
});

Testing Error Handling:

test('handles errors in async functions', async () => {
  // Using .rejects with async/await
  await expect(fetchDataWithError()).rejects.toThrow('Network Error');
  
  // Alternative approach
  try {
    await fetchDataWithError();
    // If we reach this line, the test should fail
    expect(true).toBe(false); // This will never pass
  } catch (error) {
    expect(error.message).toBe('Network Error');
  }
});

Guided Project

Testing Implementation

Build a test suite for a Node.js application using Jest

View Project

Resources

Tools & Libraries

  • Jest - JavaScript Testing Framework
  • jest-cli - Command line interface
  • jest-watch - Watch mode for Jest