JavaScript Crash Course – Lesson 7: Testing in JavaScript

Introduction to Testing

Testing is a crucial part of software development that helps ensure your code works as expected and continues to work as you make changes. In this lesson, we’ll focus on unit testing and Test-Driven Development (TDD).

Unit Testing

Unit testing involves testing individual units of code in isolation. In JavaScript, a unit is typically a function or a method of a class.

Jest: A Popular Testing Framework

We’ll use Jest, a popular JavaScript testing framework, for our examples.

To get started with Jest:

  1. Initialize a new npm project: npm init -y
  2. Install Jest: npm install --save-dev jest
  3. Update package.json:
JavaScript
{
  "scripts": {
    "test": "jest"
  }
}

Writing Your First Test

Let’s start with a simple function and its corresponding test:

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

module.exports = { add };

// math.test.js
const { add } = require('./math');

test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
});

Run the test with npm test.

Testing Asynchronous Code

Jest can also handle asynchronous code:

JavaScript
// api.js
async function fetchUser(id) {
    const response = await fetch(`https://api.example.com/users/${id}`);
    return response.json();
}

module.exports = { fetchUser };

// api.test.js
const { fetchUser } = require('./api');

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

Mocking

Mocking allows you to replace parts of your system with mock objects and make assertions about how they have been used.

JavaScript
// user.js
const api = require('./api');

async function getUserName(id) {
    const user = await api.fetchUser(id);
    return user.name;
}

module.exports = { getUserName };

// user.test.js
jest.mock('./api');
const { fetchUser } = require('./api');
const { getUserName } = require('./user');

test('getUserName calls fetchUser and returns the name', async () => {
    fetchUser.mockResolvedValue({ name: 'John Doe' });

    const name = await getUserName(1);

    expect(fetchUser).toHaveBeenCalledWith(1);
    expect(name).toBe('John Doe');
});

Test-Driven Development (TDD)

TDD is a software development process that relies on the repetition of a very short development cycle:

  1. Write a failing test
  2. Write the minimum amount of code to pass the test
  3. Refactor the code

Let’s practice TDD by implementing a simple Stack class:

JavaScript
// stack.test.js
const Stack = require('./stack');

describe('Stack', () => {
    test('is created empty', () => {
        const stack = new Stack();
        expect(stack.size()).toBe(0);
    });

    test('can push to the top', () => {
        const stack = new Stack();
        stack.push(1);
        expect(stack.size()).toBe(1);
    });

    test('can pop off the top', () => {
        const stack = new Stack();
        stack.push(1);
        expect(stack.pop()).toBe(1);
        expect(stack.size()).toBe(0);
    });
});

// stack.js
class Stack {
    constructor() {
        this.items = [];
    }

    push(element) {
        this.items.push(element);
    }

    pop() {
        if (this.items.length == 0) 
            return "Underflow";
        return this.items.pop();
    }

    size() {
        return this.items.length;
    }
}

module.exports = Stack;

Best Practices for Testing

  1. Test behavior, not implementation
  2. Keep tests simple and focused
  3. Use descriptive test names
  4. Arrange-Act-Assert pattern
  5. Don’t test external libraries or frameworks
  6. Aim for high test coverage, but don’t obsess over 100%

Practical Example: Testing the Task Manager

Let’s write tests for the Task Manager we created in the previous lesson:

JavaScript
// taskManager.js
const TaskManager = (function() {
    const tasks = new Map();

    function addTask(id, description) {
        if (!description) throw new Error('Task description is required');
        tasks.set(id, { id, description, completed: false });
    }

    function completeTask(id) {
        if (!tasks.has(id)) throw new Error('Task not found');
        const task = tasks.get(id);
        tasks.set(id, { ...task, completed: true });
    }

    function deleteTask(id) {
        if (!tasks.has(id)) throw new Error('Task not found');
        tasks.delete(id);
    }

    function getTasks() {
        return Array.from(tasks.values());
    }

    return { addTask, completeTask, deleteTask, getTasks };
})();

module.exports = TaskManager;

// taskManager.test.js
const TaskManager = require('./taskManager');

describe('TaskManager', () => {
    beforeEach(() => {
        // Clear tasks before each test
        TaskManager.getTasks().forEach(task => TaskManager.deleteTask(task.id));
    });

    test('can add a task', () => {
        TaskManager.addTask(1, 'Test task');
        expect(TaskManager.getTasks()).toHaveLength(1);
        expect(TaskManager.getTasks()[0]).toEqual({
            id: 1,
            description: 'Test task',
            completed: false
        });
    });

    test('throws error when adding task without description', () => {
        expect(() => TaskManager.addTask(1)).toThrow('Task description is required');
    });

    test('can complete a task', () => {
        TaskManager.addTask(1, 'Test task');
        TaskManager.completeTask(1);
        expect(TaskManager.getTasks()[0].completed).toBe(true);
    });

    test('can delete a task', () => {
        TaskManager.addTask(1, 'Test task');
        TaskManager.deleteTask(1);
        expect(TaskManager.getTasks()).toHaveLength(0);
    });
});

Practice Exercise

  1. Write tests for the weather app from previous lessons. Include tests for the API calls (using mocks) and the UI updates.
  2. Implement a Queue class using TDD. Include methods for enqueue, dequeue, and size.
  3. Write tests for the memoize function we created in the previous lesson.
  4. Create a simple calculator module with add, subtract, multiply, and divide functions. Write comprehensive tests for each function, including edge cases.

Conclusion

This lesson introduced the basics of testing in JavaScript, focusing on unit testing with Jest and the principles of Test-Driven Development. Testing is a crucial skill for professional developers, helping to ensure code quality, catch bugs early, and make refactoring safer.

In the next lesson, we’ll explore performance optimization in JavaScript, covering topics like efficient DOM manipulation, debouncing, and lazy loading.