JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
123: Testing Asynchronous Code
Why is my test passing even though the assertion is clearly failing?
This is the most frustrating part of starting with async testing. You write a test, you intentionally break the code to make sure the test catches it, and... it still passes. You'll feel like you're losing your mind, but what's actually happening is a "false positive."
In a synchronous test, the runner executes line by line. But with async code, if you don't tell the test runner to wait, it just triggers the promise and immediately moves to the next line. If there are no more lines, the test finishes and marks itself as "passed" before the promise ever actually resolves or throws an error.
// ❌ The "False Positive" Trap
test('fetches user data', () => {
// This function is async, but we aren't awaiting it
fetchUser(1).then(user => {
expect(user.name).toBe('Alice'); // This runs AFTER the test is already "done"
});
// The test ends right here, and the runner thinks everything is fine.
});
I've spent way too many hours debugging "passing" tests that were actually just ignoring the errors. The fix is simple: you have to make the test function itself asynchronous.
How do I actually make the test wait for the result?
The modern standard is to use async/await. It makes your test look and feel like synchronous code, which is much easier to read and maintain. You just mark the test callback as async and await the result of your function call.
Let's say we have a function that fetches a user's profile from an API. Here is how you handle that properly:
// ✅ The Correct Way
test('fetches user data successfully', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@example.com');
});
Now, the test runner knows that it needs to pause and wait for fetchUser to resolve before it proceeds to the expect statements. If the promise rejects, the test runner will catch that rejection and correctly mark the test as failed.
What do I do when I'm testing code with a long delay or a timeout?
If you're testing a function that uses setTimeout—like a debounced search input or a session timeout—you obviously don't want your test suite to actually wait 5 seconds every time it runs. That's how CI pipelines become painfully slow.
Most testing frameworks (like Jest or Vitest) provide "fake timers." Instead of waiting for real time to pass, you tell the framework to "fast-forward" the clock. I use this all the time for testing things like automatic logout timers.
// Testing a function that triggers a callback after 5 seconds
test('triggers timeout alert after 5 seconds', () => {
jest.useFakeTimers(); // Switch to fake timers
const callback = jest.fn();
startTimer(5000, callback);
// Fast-forward time by 5 seconds instantly
jest.advanceTimersByTime(5000);
expect(callback).toHaveBeenCalledTimes(1);
jest.useRealTimers(); // Always clean up!
});
Just a heads-up: remember to call useRealTimers() at the end. If you don't, other tests in your suite might behave weirdly because they're still operating on a frozen clock.
📋 Practical Task
Fixing the Flaky Weather API Test
You've been handed a test file for a getWeather(city) function. The function is asynchronous, but the current test is a "false positive"—it passes even when the city is invalid because it isn't awaiting the promise.
Your task: Modify the test code below to correctly await the getWeather call so that the test accurately fails if the returned temperature is incorrect.
async function getWeather(city) {
// Simulates an API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({ city: 'London', temp: 15 });
}, 100);
});
}
// ❌ FIX THIS TEST
test('returns the correct temperature for London', () => {
getWeather('London').then(data => {
expect(data.temp).toBe(15);
});
});
There are no comments for now.