Contents
Test Driven Development clicks when you move from theory to concrete code, so this guide walks through eight practical test driven development examples, starting with the simple stuff and leveling up to the gnarly, real world challenges you actually face: permission systems, booking races, shopping carts, and API endpoints. TDD isn’t about dogma; it’s a shift in mindset that turns testing from a chore into a design tool.
I’ve spent a lot of time teaching this. I ran a 3.5-hour tutorial on Test-Driven Development in Django with factory_boy and faker at DjangoCon US 2023, and I’ve since distilled the patterns that survive contact with real codebases into a Django TDD field guide for EuroPython 2026. The honest version of the pitch is this: TDD earns its keep at the seams of an application — permissions, state transitions, concurrency, third-party integrations — more than in the CRUD glue where you already know the shape of the interface. Two of the examples below (the permission system and the booking race) come straight from systems I’ve built and written up in detail; the rest are the classic training exercises, presented without embellishment.
1. Calculator Application with Basic Arithmetic Operations
Our first stop is the humble calculator. I know, it sounds like a “hello world” example, but stick with me. This is the dojo, the training ground. It’s a beautifully contained problem that lets you practice the core TDD rhythm — Red, Green, Refactor — without getting lost in complex logic. Before writing an add or subtract function, you first describe, in a test, what you want that function to do. This one small change flips the entire development process on its head.

The real power here isn’t proving that 2 + 2 = 4. It’s when you start thinking like a real user. What happens when someone tries to divide by zero? How should the calculator handle floating point numbers? Instead of waiting for these edge cases to become late night production bugs, TDD invites you to write a failing test for them first. This test becomes a contract, a promise that your future code must fulfill.
Strategic Breakdown
- Why it’s a Classic: It’s simple enough to grasp in minutes but has enough tricky spots (like division by zero) to make the value of TDD obvious.
- The TDD Flow in Action:
- Red: Write a test for
add(2, 3)and expect it to return 5. Of course, it fails. Theaddfunction doesn’t even exist. This is a good thing! We’ve defined a clear goal. - Green: Write the absolute simplest code to make the test pass. Seriously. You could even just write
def add(a, b): return 5. Now, add a second test foradd(4, 6). Watch it fail. Now you’re forced to write the real logic:return a + b. - Refactor: The code is tiny, so not much to refactor yet. But as you add more operations, you might spot ways to clean up your code, all while your tests ensure you don’t break anything.
- Red: Write a test for
Actionable Takeaway: Use the calculator as a “kata,” a practice routine for your team. It’s a low stakes way to build the muscle memory for thinking test first. It’s one of the most effective test driven development examples for getting everyone comfortable with the rhythm.
2. String Utility Library Development
Now let’s build something genuinely useful: a string utility library. Almost every application needs to sanitize, format, or slice up text. By starting with tests, you define exactly how you want your strings to behave. This forces you to face the messy reality of string manipulation head on, instead of discovering it when a user pastes in an emoji or a null value and crashes the app.
What should a truncate function do with an empty string? How does your sanitize function handle different character encodings? Instead of guessing and writing defensive code, you write an explicit test for each scenario. Each test is a specific question, and the code you write is the answer. This creates a rock solid, well documented contract for each function.
Strategic Breakdown
- Why it’s a Classic: It’s a common programming task that is full of hidden traps like empty strings, null values, and special characters.
- The TDD Flow in Action:
- Red: Write a test for a
reverse("hello")function, expecting “olleh”. It fails becausereverseis just a figment of our imagination. - Green: Implement the simplest possible code to pass, maybe
return input.split('').reverse().join(''). Now, level up. Add a test forreverse("")expecting"". Then another for a null input. Make them all pass. - Refactor: The first version is okay. But as you add more functions like
capitalizeortruncate, you might notice you’re checking for null inputs everywhere. This is a perfect signal to refactor that shared logic into a helper, keeping your code DRY (Don’t Repeat Yourself).
- Red: Write a test for a
Actionable Takeaway: When building your string utilities, write the “unhappy path” tests first. Create a contract for how your functions will handle empty strings, null values, and whitespace. This builds resilience in from the very start.
3. User Authentication and Authorization System
Alright, let’s raise the stakes with the first example drawn from real production code. Modern Django apps often stack multiple permission layers — view-level DRF permissions on top of object-level permissions via django-guardian or row-level security. When layers stack, precedence bugs are a real risk: a bug in one layer hides behind the other, because the app only ever shows you whichever layer is more restrictive. The rules end up complex and implicit, and nobody can say with confidence who is allowed to do what.

TDD fixes this by making the rules explicit before the code exists. Instead of writing dozens of fragmented tests, structure the whole thing as a permission matrix: a pytest.mark.parametrize table where each row is a role, an object-permission state, and the exact HTTP status you expect back. The pattern, with full code, is in my Django TDD field guide; the shape looks like this:
@pytest.mark.parametrize(
"role, has_object_perm, expected_status",
[
("admin", False, status.HTTP_200_OK),
("editor", True, status.HTTP_200_OK),
("editor", False, status.HTTP_403_FORBIDDEN),
("viewer", False, status.HTTP_403_FORBIDDEN),
("anonymous", False, status.HTTP_401_UNAUTHORIZED),
],
)
Strategic Breakdown
- Why it Matters: It operates in a high stakes domain where mistakes have serious consequences, and the matrix is the security policy — readable by a reviewer, enforced by CI.
- The TDD Flow in Action:
- Red: Write the matrix first. Every row fails, because neither the endpoint nor the permission classes exist. You have just written your security policy as an executable document.
- Green: Implement the view and permission classes until every row passes. Notice the last row: an anonymous user must get
401 Unauthorized, not403 Forbidden. Conflating “we don’t know who you are” with “we know you, and no” is one of the most common permission bugs, and the matrix forces you to decide it explicitly. - Refactor: With the matrix green, you can swap the implementation — hardcoded checks become proper password hashing, a custom check becomes
django-guardian— and the tests don’t change at all. That’s the safety net doing its job.
Actionable Takeaway: Test each permission layer in isolation before testing them stacked, and always write matrix rows for 401 vs 403. The failing row you add for “editor without object permission” today is the data leak you don’t ship next quarter.
4. E Commerce Shopping Cart Implementation
Let’s talk about money. An e commerce shopping cart is where business logic and code collide. A single bug in the total calculation or discount application can directly impact revenue. This is a perfect scenario where TDD isn’t just a development practice; it’s a business necessity. Before a user can add a single item, we must define, in code, what a “correct” cart looks like.

This example shines because it’s all about managing complex state and rules. What happens when a discount code is applied? How is sales tax calculated for different regions? Can a user add an out of stock item to their cart? TDD prompts us to answer these questions by writing a failing test for each scenario first. This process creates an ironclad safety net, ensuring every piece of business logic works exactly as intended before it ever touches a real customer’s wallet.
Strategic Breakdown
- Why it’s a Classic: It moves beyond simple algorithms into stateful application logic, which is where many tricky bugs hide. It shows how tests can serve as living documentation for complex business rules.
- The TDD Flow in Action:
- Red: Write a test called
test_add_item_to_empty_cart. Assert that after adding an item, the cart’s total price matches the item’s price and the item count is 1. The test fails because ourCartis just a dream. - Green: Create the most basic
Cartclass andadd_itemmethod to make the test pass. It might just be a simple list of items and a loop to calculate the total. - Refactor: Now add a new test,
test_add_same_item_twice_updates_quantity. This will likely fail. You’ll need to refactor youradd_itemlogic to be smarter. This iterative process of adding a test and refining the code is the heart of TDD.
- Red: Write a test called
Actionable Takeaway: Use a “test data builder” pattern. Instead of manually creating complex carts with multiple items and discounts in every single test, a builder can generate these scenarios for you. This keeps your tests clean, readable, and focused on the one thing you’re trying to prove.
5. RESTful API Endpoint Development
An API is a contract. It’s a promise your service makes to its consumers. With Test Driven Development, you write the terms of that contract first. Instead of manually testing your endpoints with a tool like Postman after you’ve built them, you codify the expected requests, responses, status codes, and error messages into an automated test suite. Your tests become living, executable documentation for your API.
The real magic happens when you start testing for the messy, real world interactions. How should your API respond to a malformed request body? What status code does it return when a user asks for a resource they don’t have permission to see? And when the endpoint calls something you don’t own — a payment gateway, an LLM, another team’s microservice — mock at that seam, not in the middle of your own logic. (The “mock boundary” rule, and how to keep mocks from drifting out of sync with the real vendor, is covered in the field guide.)
Strategic Breakdown
- Why it’s a Classic: It forces you to think like a consumer of your own API from day one, considering everything from headers and validation to proper HTTP status codes.
- The TDD Flow in Action:
- Red: Write a test for a
GET /api/widgets/1endpoint. Use a testing client to make the request and assert that the response status is 200 and the body contains the widget data you expect. It will fail because the route doesn’t exist. - Green: Create the minimal route handler and logic to fetch the widget and return it as JSON, just enough to make the test pass. Now, write a new failing test for
GET /api/widgets/999(a widget that doesn’t exist) and expect a 404 Not Found. Implement the logic to make that pass. - Refactor: As you add
POST,PUT, andDELETEendpoints, you’ll start to see repeated logic. Maybe you’re fetching objects or checking permissions in the same way. This is your cue to refactor that logic into shared helpers or middleware, all while your tests ensure you don’t break the contract.
- Red: Write a test for a
Actionable Takeaway: Let your test files mirror your API structure (e.g.,
tests/api/test_widgets.py). This makes it easy for a new developer to understand an endpoint’s complete behavior—successes, failures, and validation rules—just by reading the tests. For deeper insights, you can learn more about REST API design principles to strengthen your test first approach.
6. Data Validation and Business Rule Engine
The business logic is the heart of your application, and a validation engine is its guardian. It ensures that only clean, correct data gets into your system. Using TDD to build this engine isn’t just a good idea; it’s a strategic move. By defining your business rules as a series of tests first, you create an executable specification of what your system considers valid, from a simple email format to a complex rule like “a user’s discount code is only valid if their total purchase is over $50.”
This approach turns abstract requirements into concrete, verifiable code. Instead of hoping your if statements cover all the edge cases, you write tests that explicitly define them. What if a user’s age is exactly the minimum required? What if a dependent field is missing? TDD forces you to confront these scenarios. This makes your validation logic not just robust, but also self documenting.
Strategic Breakdown
- Why it’s a Classic: It directly translates business requirements into testable code, making it a perfect example of tests as living documentation.
- The TDD Flow in Action:
- Red: Write a test for a
UserRegistrationValidatorthat checks if theagefield is below 18. Assert that it produces a specific validation error. The test fails because the validator doesn’t exist. - Green: Implement the simplest possible validator to make the test pass, maybe a basic
if age < 18check. - Refactor: As you add more rules (password complexity, username uniqueness), you’ll see an opportunity to refactor. You could create a more generic rule engine where individual rules are small, composable objects. This makes your validation logic cleaner and easier to maintain. For instance, you could find helpful tips on validating raw JSON post request bodies for a Django backend by reading more on this topic at kdpisda.in.
- Red: Write a test for a
Actionable Takeaway: Before writing code, write down your business rules in plain English. Then, turn each rule into a failing test. This ensures you have 100% test coverage for your business logic and creates a powerful safety net that protects your core rules from accidental changes.
7. Database Constraints: Testing a Concurrent Booking System
Here is the second example from real production code, and my favorite on this list, because it’s a case where the test you write first changes the architecture you build. On a booking platform I built for a meditation facility, two users could tap “Book now” on the last seat — or the same room for overlapping dates — at the same instant. The naive implementation reads the count, sees space, and inserts. Check-then-act. It passes every test you run alone, and it double-books under load, silently, with no exception anywhere.
Try to write the failing test for that and you learn something important: you can’t reliably reproduce a race with application-level assertions, because the bug lives in the timing between two requests. The TDD-friendly move is to change the question. Instead of “does my code check availability correctly?”, the test becomes “does the database refuse to represent a double booking at all?” — which pushes the invariant down into Postgres: an exclusion constraint for overlapping room stays, a SELECT ... FOR UPDATE row lock for counted seats. Now the property is testable deterministically. The full write-up, with models, migrations, and the service layer, is here: Ambarsthan Bookings: Let the Database Win the Race.
Strategic Breakdown
- Why it Matters: It shows TDD acting as a design pressure. The hard-to-test version (check-then-act in Python) gives way to an easy-to-test version (a constraint in the schema) — and the easy-to-test version is also the correct one.
- The TDD Flow in Action:
- Red: Write a test that creates a confirmed stay for room 12 from the 4th to the 7th, then attempts a second confirmed stay from the 6th to the 9th, and asserts
IntegrityError. It fails — nothing stops the overlap yet. - Green: Add the Postgres
ExclusionConstraint(same room, overlappingtstzrange) in a migration. The test passes, and unlike anifstatement, the guarantee holds for every write path — API, Celery task, or someone in a shell. - Refactor: Add the boundary test: a stay ending at noon and one starting at noon must not clash (half-open ranges make check-out day equal to check-in day). Then test the service layer: the losing insert should surface as a clean
409 Conflictwith a “just_taken” code, matched on the constraint name — any otherIntegrityErroris a real bug and must re-raise.
- Red: Write a test that creates a confirmed stay for room 12 from the 4th to the 7th, then attempts a second confirmed stay from the 6th to the 9th, and asserts
Actionable Takeaway: When a correctness property is hard to test from application code, that’s often a sign it belongs in a lower layer. Test the invariant where it’s enforced. And use
factory_boyfactories, not JSON fixtures, to set up exactly the two competing bookings the test needs and nothing else.
8. State Machine and Workflow Engine Implementation
Complex processes like an order fulfillment pipeline — or the booking lifecycle from the last example, with its pending, confirmed, and cancelled states — can quickly turn into a tangled mess of if/else statements that nobody understands. A state machine brings order to this chaos. Applying TDD here is like drawing a map before you enter a dense forest. You define every possible state, every valid transition, and every side effect with absolute clarity before you get lost in the implementation details.
The real magic is in testing the negatives. It’s not just about proving an order can go from Processing to Shipped. It’s about writing a test that proves an order cannot jump from Pending directly to Delivered. By encoding these rules in your test suite first, you build a robust system that prevents impossible things from happening.
Strategic Breakdown
- Why it’s a Classic: It directly tames complexity. TDD provides the perfect framework to define the rules of a complex system before you write a single line of state management code.
- The TDD Flow in Action:
- Red: Write a test asserting that a newly created
Orderis in thePendingstate. It fails. Then write a test that says callingprocess_order()on it should change its state toProcessing. That fails too. - Green: Implement the minimal code to make those tests pass. Now, add a test to ensure that calling
ship_order()on aPendingorder throws an error. Watch it fail, then implement the logic to prevent this illegal move. - Refactor: As you add more states (
Shipped,Cancelled), you might decide to use a dedicated state machine library instead of simple properties. Your existing tests become your safety net, ensuring this major refactor doesn’t break any of your carefully defined rules.
- Red: Write a test asserting that a newly created
Actionable Takeaway: Use a state diagram as your testing blueprint. For every arrow on your diagram, write a test that proves the transition works. For every two states that don’t have an arrow between them, write a test that proves the transition is forbidden. This turns your visual design into a comprehensive, executable test suite.
Test Driven Development: 8 Example Comparison
| Example | Implementation Complexity | Resource Requirements | Expected Outcomes | Ideal Use Cases | Key Advantages |
|---|---|---|---|---|---|
| Calculator Application with Basic Arithmetic Operations | Very low — simple functions and tests | Minimal — unit test framework only | Solid TDD fundamentals, quick red green refactor cycles | TDD onboarding, demos, beginner exercises | Fast feedback, clear pass/fail criteria |
| String Utility Library Development | Low–moderate — multiple transformations and edge cases | Small — test data sets, encoding/regex considerations | Reliable string utilities with edge case coverage | Utility libraries, input sanitization, formatting tools | Reusable functions, comprehensive edge case tests |
| User Authentication and Authorization System | High — layered permissions, tokens, 401 vs 403 semantics | High — role fixtures, permission libraries, parametrized matrices | Explicit, executable security policy; fewer permission regressions | Applications requiring login, RBAC, object-level permissions | Catches security issues early; enables safe refactoring |
| E Commerce Shopping Cart Implementation | High — stateful logic, pricing and concurrency | Moderate–high — monetary libs, inventory integration, fixtures | Correct pricing, discount/tax rules, concurrency safe carts | Retail platforms, checkout systems, order management | Prevents pricing bugs; validates business rules and persistence |
| RESTful API Endpoint Development | Moderate — request/response and status handling | Moderate — HTTP mocking, serialization, auth stubs | Stable API contract, correct status codes and error formats | Client server integrations, microservices, public APIs | Tests define API contract; enables parallel client/server work |
| Data Validation and Business Rule Engine | Moderate — many conditional and cross field rules | Moderate — rule libraries, localization resources | Consistent validation, clear error messages, rule reuse | Forms, enterprise business rules, input validation layers | Makes business rules explicit and testable; reduces input bugs |
| Database Constraints: Testing a Concurrent Booking System | Moderate–high — constraints, transactions, race semantics | High — real Postgres in tests, factories, migration tooling | Double bookings impossible by construction; deterministic race tests | Bookings, reservations, inventory, any contended resource | Tests the invariant where it's enforced; guarantee covers every write path |
| State Machine and Workflow Engine Implementation | High — transitions, guards, side effects, async flows | High — workflow frameworks, event systems, complex mocks | Predictable workflows, enforced valid transitions and side effects | Order processing, approvals, onboarding, pipelines | Explicit state transitions; prevents illegal states and unexpected side effects |
Your Turn to Build with Confidence
We’ve journeyed from a simple calculator to database-enforced booking invariants. Across all these scenarios, a clear pattern emerges. Test Driven Development isn’t really about writing tests. It’s a design practice. It forces clarity, predictability, and simplicity into the chaotic process of building software.
The real magic is letting the tests guide you. Each failing test is a question: “What should the code do next?” Each passing test is a confirmation: “Okay, we’ve achieved that goal.” The refactor step is where we polish the story, making it not just correct, but elegant and easy for the next person to understand.
Distilling the Core Lessons
Let’s pause and reflect on what we’ve learned from these test driven development examples.
- Tests as Design Documentation: The permission matrix in example 3 isn’t just a test; it’s the security policy, readable by any reviewer. The API tests document the exact JSON structure and error codes. A new developer can understand the contract without ever seeing the implementation.
- Tests as Design Pressure: The booking example showed the strongest version of this. When a property was nearly impossible to test from application code, TDD pushed the invariant down into the database — and the testable design turned out to be the correct one.
- Confidence in Refactoring: The authentication and shopping cart systems are guaranteed to change. Business rules evolve. With a comprehensive test suite, you can refactor those critical pieces knowing the tests are a safety harness that will catch you if you make a mistake.
The goal of TDD is not to have a suite of tests. The goal is to have a well designed, maintainable system. The test suite is a wonderful, confidence boosting side effect.
Your Actionable Path Forward
Theory is easy, but the real learning happens when you write the code. You don’t need to rewrite your entire application overnight. Instead, start with the smallest possible step.
- Pick One Small Feature: Look at your next task. Find a single, small, well defined piece of work. A new API endpoint, a small utility function, a single component.
- Commit to the Cycle: For just that one feature, commit to the “Red, Green, Refactor” cycle. Write a failing test first. Write the minimum code to make it pass. Then, clean it up.
- Embrace the “Slowness”: It will feel slower at first. That’s okay. You’re trading frantic typing for focused thinking.
- Go Deeper on the Patterns: When you’re ready for the production-scale version — factory taxonomies, mock boundaries, permission matrices, and the anti-patterns that make test suites flaky — the Django TDD field guide covers them in depth, and my DjangoCon US 2023 tutorial builds a tested Django API from scratch over 3.5 hours.
The test driven development examples in this article are your map. They show you the terrain. Now, it’s your turn to take the first step.
Struggling with technical debt or trying to build a strong engineering culture? As a consultant specializing in scalable architecture, I help teams implement practices like TDD to build better products, faster. Let’s connect at Kuldeep Pisda and talk about building your next feature with unshakable confidence.
