2907 words
15 minutes
How to get started with tdd (test-driven development) in python and javascript

Getting Started with Test-Driven Development (TDD) in Python and JavaScript#

Test-Driven Development (TDD) is a software development methodology that prescribes writing automated tests before writing the functional code. This approach fundamentally shifts the developer’s perspective, emphasizing the desired behavior of the code from the outset. TDD is widely adopted across various programming languages and paradigms, proving particularly effective in Python and JavaScript development environments. Adopting TDD can lead to higher code quality, improved design, and increased developer confidence.

What is Test-Driven Development (TDD)?#

At its core, TDD is a design technique and a development process, not just a testing practice. The workflow follows a strict cycle known as “Red-Green-Refactor.”

  • Red: Write a small automated test that describes a desired piece of functionality. This test should initially fail because the corresponding code does not yet exist or does not meet the requirement.
  • Green: Write the minimum amount of production code necessary to make the failing test pass. The focus here is solely on passing the test quickly, even if the code is not optimally designed or efficient.
  • Refactor: Improve the code (both the test and the production code). This might involve simplifying code, removing duplication, improving readability, or enhancing efficiency, without changing its behavior. After refactoring, run the tests again to ensure the changes did not introduce regressions.

This cycle repeats for each small increment of functionality being added.

TDD contrasts with traditional development flows where code is written first, and tests are added afterward to verify its behavior. By writing tests first, TDD encourages developers to think about the requirements and the public interface of their code before implementation details. This often leads to simpler, more modular designs.

Commonly cited benefits of practicing TDD include:

  • Improved Design: Forces consideration of how code will be used, promoting modularity and smaller, focused units.
  • Reduced Bugs: Writing tests first catches defects early in the development cycle. The comprehensive test suite acts as a safety net for future changes.
  • Increased Confidence: Developers can refactor or add features with greater confidence, knowing the tests will immediately signal if something breaks.
  • Executable Documentation: The tests serve as clear examples of how the code is intended to be used.
  • Faster Iteration (Long Term): While initial setup might feel slower, the reduction in debugging time and the safety net for refactoring accelerate development over the project’s lifecycle.

Core Concepts for Practicing TDD#

Understanding a few fundamental concepts is essential before starting with TDD:

  • Unit Tests: TDD primarily focuses on unit tests, which test the smallest possible units of code in isolation (e.g., a single function, method, or class). Dependencies are often “mocked” or “stubbed” to isolate the unit under test.
  • Test Cases: A specific scenario designed to test a particular aspect of the unit’s behavior, typically involving specific inputs and expected outputs or side effects.
  • Assertions: Statements within a test case that check if a condition is true (e.g., checking if the function’s output equals the expected value, or if an exception was raised). If an assertion fails, the test case fails.
  • Test Suite: A collection of test cases, often grouped logically by the unit or feature they are testing.
  • Test Runner: A program or tool that discovers and executes tests, reporting the results (which tests passed, which failed).

The Red-Green-Refactor cycle is the operational core of TDD. It provides a rhythm and structure for the development process.

  1. Think: Understand the specific requirement or behavior to implement.
  2. Red: Write a test that verifies only this requirement and fails.
  3. Run Tests: Execute the test suite. Confirm the new test fails and all previous tests pass.
  4. Green: Write just enough code to make the failing test pass. Do not implement more functionality than required by the current test.
  5. Run Tests: Execute the test suite again. Confirm all tests (including the new one) pass.
  6. Refactor: Improve the design of the code or tests while keeping all tests passing. Remove duplication, clarify names, simplify logic.
  7. Repeat: Select the next small requirement and start the cycle again.

TDD in Python: Getting Started#

Python has robust testing support built into the language and a rich ecosystem of third-party testing frameworks. Two popular choices for TDD in Python are the built-in unittest module and the highly favored pytest framework. pytest is often recommended for new projects or teams adopting TDD due to its simpler syntax and powerful features.

Setting up Pytest#

  1. Installation: Install pytest using pip:
    Terminal window
    pip install pytest
  2. Project Structure: A common convention is to place test files in a tests/ directory at the root of the project. Test files are typically named starting with test_ (e.g., test_myapp.py). Test functions within these files also start with test_.

Python TDD Example (Using Pytest)#

Consider building a simple function is_even(number) that checks if an integer is even.

Step 1: Think

The requirement is a function that takes an integer and returns True if it’s even, False otherwise. A simple starting point is testing a known even number, like 2.

Step 2: Red - Write a Failing Test

Create a file tests/test_math.py with the following content:

tests/test_math.py
# Need to import the function we will create
# from myapp.utils import is_even # This line will cause an error initially
def test_is_even_for_even_number():
# We expect is_even(2) to return True
# assert is_even(2) is True # This assertion will fail because is_even doesn't exist
pass # Placeholder to allow syntax checking before writing the failing assertion

And in your application code directory (e.g., myapp/), create an empty utils.py file:

myapp/utils.py
# Nothing here yet

Step 3: Run Tests (Red)

Navigate to the project root in your terminal and run pytest:

Terminal window
pytest

This will likely result in an error indicating is_even is not defined or cannot be imported. Let’s add the assertion and the import, uncommenting the lines in tests/test_math.py:

tests/test_math.py
from myapp.utils import is_even # Now this line is expected to work after creating myapp/utils.py
def test_is_even_for_even_number():
# We expect is_even(2) to return True
assert is_even(2) is True

Now run pytest again. The test will fail, likely with an ImportError or NameError because myapp.utils.is_even doesn’t exist.

============================= test session starts ==============================
...
ImportError: cannot import name 'is_even' from 'myapp.utils' (.../myapp/utils.py)
============================== 1 error in ...s ===============================

This is our Red state.

Step 4: Green - Write Minimum Code to Pass

Write just enough code in myapp/utils.py to satisfy the current test (test_is_even_for_even_number):

myapp/utils.py
def is_even(number):
# Minimal code to pass test_is_even_for_even_number
# This is clearly not the correct implementation, but it passes the test for 2
if number == 2:
return True
return False # Doesn't matter yet for the current test

Step 5: Run Tests (Green)

Run pytest again:

Terminal window
pytest

This time, the test should pass.

============================= test session starts ==============================
...
collected 1 item
tests/test_math.py . [100%]
============================== 1 passed in ...s ===============================

We are in the Green state.

Step 6: Refactor (and Repeat)

Now, we know the code is wrong (it only works for 2). This is where TDD shines. We add another test for a different number or edge case. Let’s test an odd number.

Step 1 (Repeat): Think

Test an odd number, like 3. It should return False.

Step 2 (Repeat): Red - Write a Failing Test

Add a new test function to tests/test_math.py:

tests/test_math.py
from myapp.utils import is_even
def test_is_even_for_even_number():
assert is_even(2) is True
def test_is_even_for_odd_number():
assert is_even(3) is False # This will fail with the current implementation

Step 3 (Repeat): Run Tests (Red)

Run pytest. The first test will pass, the new one will fail.

============================= test session starts ==============================
...
collected 2 items
tests/test_math.py .F [100%]
=================================== FAILURES ===================================
___________________________ test_is_even_for_odd_number ________________________
def test_is_even_for_odd_number():
> assert is_even(3) is False
E AssertionError: assert False is False
E + where False = is_even(3)
tests/test_math.py:10: AssertionError
=========================== short test summary info ============================
FAILED tests/test_math.py::test_is_even_for_odd_number - AssertionError: asser...
========================= 1 failed, 1 passed in ...s =========================

We are back in the Red state.

Step 4 (Repeat): Green - Write Minimum Code to Pass

Update myapp/utils.py to pass both tests:

myapp/utils.py
def is_even(number):
# Still minimal, but now covers both 2 and 3
if number == 2:
return True
if number == 3: # Added this to pass the new test
return False
return False

Step 5 (Repeat): Run Tests (Green)

Run pytest again. Both tests should pass.

============================= test session starts ==============================
...
collected 2 items
tests/test_math.py .. [100%]
============================== 2 passed in ...s ===============================

We are in the Green state again.

Step 6 (Repeat): Refactor

The code still isn’t the correct general solution. The tests pass, so we can refactor confidently. Change myapp/utils.py to use the modulo operator:

myapp/utils.py
def is_even(number):
"""Checks if a number is even."""
return number % 2 == 0 # Correct general implementation

Run pytest one last time. All tests still pass. The code is now correct and all existing tests verify its behavior. We can add more tests (e.g., for 0, negative numbers, large numbers, non-integers if required by specification) by repeating the Red-Green-Refactor cycle.

This small example demonstrates the core TDD loop in Python with pytest.

TDD in JavaScript: Getting Started#

JavaScript’s testing landscape is vast, with many frameworks and libraries. Popular choices for TDD include Jest, Mocha, and Vitest. Jest is often a good starting point due to its integrated assertion library, mocking capabilities, and ease of setup, particularly in React or Node.js projects.

Setting up Jest#

  1. Installation: Install Jest as a development dependency using npm or yarn:
    Terminal window
    npm install --save-dev jest
    # or
    yarn add --dev jest
  2. Configuration: Jest often works with zero configuration, but adding a test script to package.json is standard.
    package.json
    {
    "scripts": {
    "test": "jest"
    }
    }
  3. Project Structure: Test files are typically placed alongside the code they test (e.g., utils.test.js for utils.js) or in a dedicated __tests__ directory. Jest automatically finds files ending with .test.js, .test.ts, .spec.js, or .spec.ts.

JavaScript TDD Example (Using Jest)#

Let’s build the same isEven(number) function in JavaScript.

Step 1: Think

The requirement is the same: a function returning true for even integers, false for odd. Start with testing isEven(2).

Step 2: Red - Write a Failing Test

Create a file isEven.test.js with the following content. Assume the function will be in isEven.js.

isEven.test.js
// Import the function we will create
// const isEven = require('./isEven'); // This will fail initially if isEven.js is empty or doesn't export
// Jest test suite
describe('isEven', () => {
// Test case for an even number
test('should return true for an even number', () => {
// Expect isEven(2) to be true
// expect(isEven(2)).toBe(true); // This assertion will fail
});
});

Create an empty isEven.js file:

isEven.js
// Nothing here yet

Step 3: Run Tests (Red)

Run Jest from the terminal using the script defined in package.json:

Terminal window
npm test
# or
yarn test

This will fail because isEven is not defined. Uncomment the import and the assertion in isEven.test.js:

isEven.test.js
const isEven = require('./isEven'); // Now this import is expected to work
describe('isEven', () => {
test('should return true for an even number', () => {
expect(isEven(2)).toBe(true);
});
});

Run npm test again. It will fail because isEven is not a function or returns an incorrect value.

FAIL ./isEven.test.js
isEven
✕ should return true for an even number (5ms)
● isEven › should return true for an even number
TypeError: isEven is not a function
10 | describe('isEven', () => {
11 | test('should return true for an even number', () => {
> 12 | expect(isEven(2)).toBe(true);
| ^
13 | });
14 | });
at Object.<anonymous> (isEven.test.js:12:12)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: ...
Ran all test suites matching /isEven.test.js/i.

This is the Red state.

Step 4: Green - Write Minimum Code to Pass

Write just enough code in isEven.js to make the current test pass:

isEven.js
const isEven = (number) => {
// Minimal code to pass the test for 2
if (number === 2) {
return true;
}
return false; // Doesn't matter yet for the current test
};
module.exports = isEven; // Export the function

Step 5: Run Tests (Green)

Run npm test again:

Terminal window
npm test

This time, the test should pass.

PASS ./isEven.test.js
isEven
✓ should return true for an even number (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: ...
Ran all test suites matching /isEven.test.js/i.

We are in the Green state.

Step 6: Refactor (and Repeat)

The code is incorrect for numbers other than 2. Add a test for an odd number, like 3.

Step 1 (Repeat): Think

Test an odd number, 3. It should return false.

Step 2 (Repeat): Red - Write a Failing Test

Add a new test case to isEven.test.js:

isEven.test.js
const isEven = require('./isEven');
describe('isEven', () => {
test('should return true for an even number', () => {
expect(isEven(2)).toBe(true);
});
test('should return false for an odd number', () => {
expect(isEven(3)).toBe(false); // This will fail with the current implementation
});
});

Step 3 (Repeat): Run Tests (Red)

Run npm test. The first test passes, the new one fails.

FAIL ./isEven.test.js
isEven
✓ should return true for an even number (5ms)
✕ should return false for an odd number (2ms)
● isEven › should return false for an odd number
expect(received).toBe(expected) // Object.is equality
Expected: false
Received: true
18 |
19 | test('should return false for an odd number', () => {
> 20 | expect(isEven(3)).toBe(false);
| ^
21 | });
22 | });
at Object.<anonymous> (isEven.test.js:20:23)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: ...
Ran all test suites matching /isEven.test.js/i.

We are back in the Red state.

Step 4 (Repeat): Green - Write Minimum Code to Pass

Update isEven.js to pass both tests:

isEven.js
const isEven = (number) => {
// Still minimal, but now covers both 2 and 3
if (number === 2) {
return true;
}
if (number === 3) { // Added this
return false;
}
return false;
};
module.exports = isEven;

Step 5 (Repeat): Run Tests (Green)

Run npm test again. Both tests should pass.

PASS ./isEven.test.js
isEven
✓ should return true for an even number (4ms)
✓ should return false for an odd number (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: ...
Ran all test suites matching /isEven.test.js/i.

We are in the Green state.

Step 6 (Repeat): Refactor

Refactor the code to the correct general solution using the modulo operator:

isEven.js
const isEven = (number) => {
return number % 2 === 0; // Correct general implementation
};
module.exports = isEven;

Run npm test one last time. All tests still pass. The code is now correct and verified by the tests. Additional test cases (0, negatives, edge cases) can be added by repeating the Red-Green-Refactor cycle.

This example illustrates the TDD workflow in JavaScript using Jest.

Practical Tips for Adopting TDD#

Adopting TDD requires discipline and practice. Here are some practical tips:

  • Start Small: Do not attempt to apply TDD to an entire large, complex system at once. Begin by applying it to new, isolated features or modules. Practice on small, well-defined problems.
  • Focus on Unit Tests: TDD is most effective at the unit level. While other test types (integration, end-to-end) are important, the core TDD cycle operates on small, testable units of code.
  • Keep Tests Fast: Tests should run quickly. A slow test suite discourages running tests frequently, breaking the TDD rhythm. Mocking external dependencies (databases, APIs) is crucial for speed and isolation.
  • Write Readable Tests: Tests serve as documentation. Write clear, concise test names and use expressive assertions. A test should clearly articulate the behavior being verified.
  • Automate Test Execution: Integrate test running into the build process or use file watchers that automatically run tests when code changes. This reinforces the habit of running tests after every small change.
  • Refactor Aggressively (When Green): The Green state is the signal that it is safe to improve the code’s internal structure. Do not skip the Refactor step. It is vital for maintaining code health.
  • Team Adoption: If working in a team, TDD requires collective buy-in and consistent practice. Establish team standards for writing tests and following the cycle.
  • Know When to Pause: TDD is not a silver bullet. While applicable to most logic, it might be less intuitive or efficient for exploring initial UI layouts, dealing with complex, unstable external systems, or writing simple one-off scripts where formal verification adds little value.

Real-World Application and Insights#

TDD is practiced successfully across various industries and project types. Companies leveraging TDD report several benefits in practice:

  • Improved Code Maintainability: Code developed with TDD tends to be more modular and easier to understand due to the focus on smaller units and clear interfaces. This makes future maintenance and feature additions less prone to error.
  • Higher Developer Morale: The safety net provided by a comprehensive test suite significantly reduces the fear of making changes, allowing developers to work faster and with greater confidence.
  • Better Collaboration: Tests provide a common ground for understanding requirements and code behavior among team members.
  • Faster Onboarding: New team members can use the test suite as a guide to understand how different parts of the system work and how to interact with them.

Empirical studies on the effectiveness of TDD exist, with some suggesting that teams practicing TDD may produce code with lower defect density compared to teams not using TDD, although they might experience a slight increase in initial development time. The balance often favors TDD for projects where long-term maintainability, stability, and quality are critical.

Key Takeaways#

  • Test-Driven Development (TDD) is a development methodology focusing on writing automated tests before writing functional code.
  • The core of TDD is the Red-Green-Refactor cycle: Write a failing test (Red), write minimal code to pass (Green), improve code and tests (Refactor).
  • TDD is primarily practiced using unit tests, verifying the smallest code units in isolation.
  • In Python, pytest is a popular and user-friendly framework for TDD.
  • In JavaScript, Jest is a widely used framework suitable for TDD due to its integrated features.
  • Adopting TDD involves discipline, starting small, focusing on unit tests, keeping tests fast, and integrating test execution into the workflow.
  • TDD contributes to better code design, fewer bugs, increased confidence during refactoring, and serves as living documentation.
  • While requiring initial discipline, TDD often leads to faster, more confident development and higher quality code in the long run.
How to get started with tdd (test-driven development) in python and javascript
https://dev-resources.site/posts/how-to-get-started-with-tdd-testdriven-development-in-python-and-javascript/
Author
Dev-Resources
Published at
2025-06-26
License
CC BY-NC-SA 4.0