-
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)
125: Test-Driven Development Basics in JavaScript
A few years ago, I watched a teammate spend an entire Friday night hunting a bug in a tax calculation utility. He had changed one conditional to handle a new tax region in Canada, but in doing so, he accidentally inverted the logic for the existing US states. He didn't realize he'd broken the core functionality until a customer reported a checkout error on Saturday morning. He spent hours manually entering values into a form, refreshing the page, and praying he'd find the edge case. That's the exact nightmare Test-Driven Development (TDD) is designed to kill. When you have a suite of tests, you don't "hope" it still works; you know it does because the tests tell you in milliseconds.
Flipping the Script with Red-Green-Refactor
Most of us are taught to write code and then "test it" to see if it works. TDD flips that on its head. You write the test first, watch it fail, and then write the bare minimum amount of code required to make that test pass. I call this the "Red-Green-Refactor" loop. It sounds counterintuitive—why write a test for something that doesn't exist yet? Because it forces you to think about the interface and the expected outcome before you get bogged down in the implementation details. It stops you from over-engineering features you don't actually need.
Building a Discount Calculator from the Outside In
Let's look at how this actually feels in practice. Imagine we need a function that applies a discount code to a cart total. Instead of writing the logic, we start with a test. I'll use a generic assertion style here that you'll see in frameworks like Jest or Mocha.
// The Test (The "Red" phase)
test('should apply 10% discount for code SAVE10', () => {
const result = applyDiscount(100, 'SAVE10');
expect(result).toBe(90);
});
If you run this right now, it will crash because applyDiscount isn't defined. That's your "Red" state. Now, we move to "Green." The goal here isn't to write the perfect, scalable function; it's to make the test pass as quickly as possible. I might even hardcode the return value just to prove the test works.
// The Implementation (The "Green" phase)
function applyDiscount(price, code) {
if (code === 'SAVE10') {
return price * 0.9;
}
return price;
}
Once the test turns green, we have a safety net. If we want to add a 'SAVE20' code or handle invalid codes, we repeat the cycle: write a failing test for the new requirement, then write the code to satisfy it. You're building the feature in tiny, verifiable increments.
The Art of Refactoring without Fear
The final part of the loop is Refactoring. This is where you clean up the code—removing duplication, improving variable names, or optimizing performance. In a traditional workflow, refactoring is terrifying because you might break something. In TDD, refactoring is a breeze. If you change a for loop to a .reduce() and the tests stay green, you haven't broken the contract. If they turn red, you know exactly what you messed up and can undo it in seconds.
I'll be honest: TDD feels slow at first. You'll feel like you're writing twice as much code. But you'll save that time tenfold by not spending your weekends debugging regressions that should have been caught by a script.
📋 Practical Task
Build a Password Strength Validator using TDD
Your goal is to create a validatePassword(password) function that returns true if a password is strong and false otherwise. However, you must follow the TDD workflow strictly. Do not write the function logic until you have written a failing test for that specific requirement.
Implement the following requirements in order, following the Red-Green-Refactor cycle for each:
- Requirement 1: The password must be at least 8 characters long. (Write a test that fails for a 7-character password, then make it pass).
- Requirement 2: The password must contain at least one number. (Write a test that fails for a password with only letters, then make it pass).
- Requirement 3: The password must contain at least one special character (e.g., !, @, #, $). (Write a test that fails for alphanumeric-only passwords, then make it pass).
For the final step, refactor your validatePassword function to be as concise as possible (perhaps using Regular Expressions) and ensure all your tests still pass.
There are no comments for now.