Why Write Automated Tests?

Writing tests might seem like extra work, but it pays off in the long run.

  • Confidence: You can refactor or add new features and be confident you haven't broken existing functionality.
  • Bug Prevention: Tests catch bugs early in the development process, when they are cheapest to fix.
  • Living Documentation: Well-written tests describe what your code is supposed to do.
Setting up Jest

Jest is a popular "zero-configuration" testing framework. Getting started is easy.

  1. Initialize your project (if you haven't already): npm init -y
  2. Install Jest as a development dependency: npm install --save-dev jest
  3. Update package.json: Add a test script.
  4. JSON

"scripts": {
  "test": "jest"
}

Now you can run your tests from the terminal with npm test.

Writing Your First Test

Jest tests are typically placed in a __tests__ directory or in files named *.test.js or *.spec.js.

Let's say we have a simple function to test in a file called math.js:

JavaScript


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

module.exports = sum;

Now, let's write a test for it in math.test.js:

JavaScript


// math.test.js
const sum = require('./math');

// `test` is a global function from Jest.
// The first argument is a description of what you're testing.
// The second argument is a function containing the test logic.
test('adds 1 + 2 to equal 3', () => {
  // `expect` returns an "expectation" object.
  // `.toBe` is a "matcher" function that checks for exact equality.
  expect(sum(1, 2)).toBe(3);
});

test('adds -5 + 10 to equal 5', () => {
  expect(sum(-5, 10)).toBe(5);
});

When you run npm test, Jest will automatically find this file, run the tests, and give you a report.

Common Matchers

The .toBe() matcher checks for exact equality (===). Jest has many other useful matchers:

  • .toEqual(value): To check the value of an object (deep equality).
  • .toBeNull(): Checks if the value is null.
  • .toBeTruthy() / .toBeFalsy(): Checks if the value is truthy or falsy.
  • .toContain(item): Checks if an array or string contains an item.
  • .toThrow(): Checks if a function throws an error.

Example using .toEqual for an object:

JavaScript


test('object assignment', () => {
  const data = { one: 1 };
  data['two'] = 2;
  // Use .toEqual for objects, not .toBe
  expect(data).toEqual({ one: 1, two: 2 });
});
Introduction to DOM Testing

Jest can also test code that interacts with the DOM. It uses a library called jsdom to simulate a browser environment right inside Node.js.

Let's say we have this HTML and a corresponding script:

HTML


<button id="my-button">Click Me</button>
<p id="message"></p>

JavaScript


// script.js
document.getElementById('my-button').addEventListener('click', () => {
  document.getElementById('message').textContent = 'Button was clicked!';
});

Here's how you could test it with Jest:

JavaScript


// dom.test.js

// 1. Set up the document body with your HTML
document.body.innerHTML = `
  <button id="my-button">Click Me</button>
  <p id="message"></p>
`;

// 2. Require the script that adds the event listener
require('./script');

test('displays a message after button click', () => {
  const button = document.getElementById('my-button');
  const message = document.getElementById('message');

  // 3. Check initial state
  expect(message.textContent).toBe('');

  // 4. Simulate a user action
  button.click();

  // 5. Assert the result
  expect(message.textContent).toBe('Button was clicked!');
});

This test creates a virtual DOM, runs your script against it, simulates a click, and then checks if the DOM was updated as expected.

Quizzes

1. In a Jest test, what is the role of a "matcher" function like .toBe() or .toEqual()?

  • A) It runs the function that is being tested.
  • B) It describes what the test is supposed to do.
  • C) It compares the actual output of your code to an expected output.
  • D) It imports the necessary files for the test.

Answer: C Explanation: A matcher is the final part of the expect(actual).matcher(expected) chain. Its job is to perform a specific type of comparison (e.g., exact equality, object equality, truthiness) between the actual result of your code and the result you expected.

2. Why would you use .toEqual() instead of .toBe() in a Jest test?

  • A) When you want to check if a function throws an error.
  • B) When you are comparing primitive values like numbers or strings.
  • C) When you are comparing the values of two different objects or arrays.
  • D) When you want the test to fail deliberately.

Answer: C Explanation: .toBe() checks for strict referential equality (===), meaning it checks if two variables point to the exact same object in memory. .toEqual() recursively checks every field of an object or array to see if they have the same values. It's the correct choice for comparing non-primitive types.

Tutorial 8: Blazing Fast JS: Performance, Memory Leaks, Repaint & Reflow

  • Difficulty: Advanced
  • Description: Is your web app feeling sluggish? Dive into key JavaScript performance concepts. Learn to identify common memory leaks that bloat your application and understand how to minimize costly browser repaint and reflow operations to keep your UI silky smooth. 🏎️
  • Time to Read: 14 minutes

Content

Understanding Memory Leaks

In memory-managed languages like JavaScript, the engine (e.g., V8 in Chrome) has a "garbage collector" that automatically frees up memory that is no longer needed. A memory leak occurs when your application holds onto references to objects it will never use again, preventing the garbage collector from reclaiming that memory. Over time, this consumes more and more RAM, leading to slowdowns and crashes.

Common Causes of Memory Leaks:

  1. Accidental Global Variables: If you forget let, const, or var, a variable can be attached to the global window object, where it will live forever.
  2. JavaScript

function createLeakyData() {
  // Whoops! 'leakyData' becomes a global variable.
  leakyData = new Array(1000000).join('*');
}
  1. Forgotten Timers or Callbacks: If a setInterval references objects and is never cleared with clearInterval, those objects can never be garbage collected.
  2. JavaScript

function leakyTimer() {
  const someBigObject = { /* ... */ };
  setInterval(() => {
    // This callback keeps 'someBigObject' alive forever
    console.log(someBigObject); 
  }, 1000);
  // We never call clearInterval!
}
  1. Detached DOM Elements: If you remove a DOM element from the page but keep a reference to it in your JavaScript, the element (and all its children) will remain in memory.

Finding Leaks: The Chrome DevTools Memory tab is your best tool. You can take a "Heap Snapshot," perform an action in your app, take another snapshot, and compare them to see which objects were created and not released.

The Rendering Pipeline: Reflow and Repaint

To display a webpage, the browser goes through a series of steps. Two of the most important (and expensive) are Layout (Reflow) and Paint (Repaint).

  • Reflow (or Layout): This is the process of calculating the exact position and size of every element on the page. A reflow on one element can trigger reflows on its parents and children. It's very expensive! Triggers: Changing an element's width, height, position (top, left), font size, or adding/removing elements from the DOM.
  • Repaint (or Paint): This is the process of filling in the pixels for each element after the layout has been calculated. It's less expensive than a reflow, but still has a performance cost. Triggers: Changing properties that don't affect layout, like background-color, color, or visibility.

The goal for smooth animations and interactions is to minimize both reflow and repaint.

How to Minimize Reflow and Repaint
  1. Batch DOM Changes: Instead of changing styles on multiple elements one by one, group them. Change a parent's CSS class rather than the individual styles of 10 children.
  2. JavaScript

// BAD: Causes 3 reflows
const el = document.getElementById('my-element');
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';

// GOOD: Causes 1 reflow
el.classList.add('new-styles'); 
/* .new-styles { width: 100px; height: 100px; margin: 10px; } */
  1. Animate with transform and opacity: Changes to transform (like translate, scale, rotate) and opacity can often be handled by the GPU and usually don't trigger a reflow. They are the best properties to use for animations.
  2. Avoid Reading Layout Properties in a Loop: When you read a property like element.offsetHeight or element.offsetLeft, you force the browser to perform a layout calculation to give you the exact value. If you do this repeatedly after making changes, you trigger multiple reflows.
  3. JavaScript

// BAD: Forces a reflow on every loop iteration
for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = (elements[i].offsetWidth + 10) + 'px';
}

// GOOD: Read all values first, then write all values
const widths = [];
for (let i = 0; i < elements.length; i++) {
  widths.push(elements[i].offsetWidth);
}
for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = (widths[i] + 10) + 'px';
}