Introduction to Test-Driven Development (TDD) with Classic TDD Example

Last updated Mar 31st, 2021
The best thing tests give us is "feedback". Feedback as to if our designs are good, if there are bugs, and if we're making progress. If part of what it means to be a good developer is to write testable code, the path of least resistance is to use a test-first approach. In this series about Test-Driven Development, we seek to master the art of testing, regardless of which side of the stack you're on (front & back).

Watch on YouTube

Introduction

Why tests?

Confidence ⭐ ⭐ ⭐ ⭐

Feedback ⭐ ⭐ ⭐ ⭐ ⭐

More reasons to write tests

Why testing is hard

Testing isn't regularly taught in schools

As an industry, we struggle to agree on testing terminology

Understanding what to test and how to test it takes practice

We leave testing until the very end

Testing is a part of architecture

The Test-Driven Development (TDD) Process

Red-Green-Refactor

Types of tests (unit, integration, E2E, acceptance)

The big picture: How TDD works on real-life projects

Starting with the acceptance test

Double Loop TDD

Classic (Inside-Out/Chicago) and Mockist (Outside-In/London) TDD

Demonstration: Classic TDD — Palindrome Example

Requirements

Rules

Getting started

1. Write the failing test

2. Write the simplest code to make the test pass

3. Refactor

4. The next failing test

5. Write the simplest code to make the test pass

6. Refactor

7. The next failing test

8. Write the simplest code to make the test pass

9. Refactor

Continue

Conclusion

Homework

Next in the series


🌱 This article hasn't fully bloomed. It's very likely to change over the next little while.

Watch on YouTube

If you're like me, you see programming as a type of trade 1.

Over the past year, I've been paying more attention to how traditional tradespeople work. I've been looking for parallels between how they work and how we work as software developers.

When I most recently took my car in to get an oil change, I noticed that the technicians seemed to consistently, reliably, and confidently get the job done, and get it done right. One client after the next. They weren't making messes. They weren't missing their estimates. They weren't discovering that things won't work at the last minute - in fact, I've even seen them come out, 2 minutes into the job to let someone know that there's more work to be done than they initially expected.

It appears, at least to me, as if these are principled workers, and — forgive the pun, but it seems like they're not reinventing the wheel.


Part of what it means to be an Agile software craftsperson is to consistently deliver value to the customer. That's hard. But that's what we're constantly trying to do here.

The goal of this blog is to discover the best techniques for writing testable, flexible, maintainable code, and to teach others how to do it too. Today, the software quality attribute we're most interested in is testability.

And in my experience:

The best way to write testable code 2 is to write the test first

Introduction

This is the first post in a series on Test-Driven Development (TDD): a test-first technique for developing software 🧪.

In this introductory post, you'll build a beginner foundation for TDD. We'll learn about what TDD is, what makes it important, and how developers are using it to consistently deliver value on real-life projects. We'll also discuss what makes testing so challenging to get right, and in the end, we'll wrap up with a demonstration of the Classic TDD process, using TDD to construct a palindrome checker.

You'll learn everything you need to know to get started practicing the TDD Red-Green-Refactor process. This is just the starting point. We need to get this foundation down first before we can learn how to apply advanced TDD techniques in the real-world 3.

Let's begin.

Why tests?

There are lots of reasons why tests are helpful, but the two best reasons, in my opinion, are confidence and feedback.

Confidence ⭐ ⭐ ⭐ ⭐

They say that change is a constant in software development.

To find the confidence to safely add, remove, or refactor code without the fear of introducing bugs and regressions, we need tests.

This is especially important later on in a project when the codebase is much larger than it was at the beginning, and there's way more code that one human being can mentally account for anymore.

How TDD progresses with and without tests

How projects with good tests, bad tests, and no tests progress over time. Without tests (or with bad tests), it's hard to continue to safely make progress because new changes introduce regressions. Refactoring without tests is a great way to introduce regressions.

I can tell you from experience that this is not a fun situation to be in— thousands of lines of code into a project with no tests and no safety. It's a great way to turn a promising codebase into an unstable mess.

Feedback ⭐ ⭐ ⭐ ⭐ ⭐

In my opinion, the most important reason for tests is feedback.

Tests give us feedback to let us know:

  • When regressions have been introduced
  • What our progress towards implementing a feature looks like
  • That we actually understand the customer requirements
  • And if our designs are feasible

Depending on how you think about it, confidence may actually come from the feedback we get from tests.

Introduction%20to%20Test-Driven%20Development%20(TDD)%20with%2022bd00e6d7044c10a96fb92c4bcc8816/testing_fundamentals_(53).png

[Principle]: Listen to your tests — Tests that are hard to write typically signal a deficiency in design. Use the immediate feedback you get from feeling out how hard it is to write a particular test to reconsider the design.

Feedback is so important that Kent Beck lists it as one of the primary values of Extreme Programming: the influential Agile software development methodology.

More reasons to write tests

  • Measure progress ⭐ ⭐
  • Sculpt out public APIs
  • Understand requirements ⭐ ⭐
  • Keep a feature in scope ⭐ ⭐
  • Documentation for other developers ⭐ ⭐ ⭐

Why testing is hard

I didn't start writing tests until later in my career. When I first started out, I knew that we should probably have them, but as for writing them? Yeah, right. Like many new developers, I was lost as to how to even get started. After surveying the landscape, here's why I think testing is hard.

Testing isn't regularly taught in schools

This may not be the case for some readers, but the majority of my college/university/bootcamp-going peers weren't exposed to how to properly test code until a mentor sat down and showed them how to do it.

As an industry, we struggle to agree on testing terminology

Ask two developers what they believe an integration test is. As Fowler writes, there are two completely different notions of what this means, and we still haven't converged on a standard.

This is true for much of the other test types as well (unit, acceptance, E2E, contract, etc).

It also appears that your organization style and role (front-end, back-end, full-stack) play a role in how we see certain types of tests as well.

Understanding what to test and how to test it takes practice

Knowing what to test is hard. And this is especially misleading for newer developers since a lot of libraries and frameworks tell you how to test code within their library or framework, but don't actually give you best practices for testing your code within them.

what-and-how-to-test-tdd

The magic rule here is to test against behavior. And if you understand the requirements, we can pretty much turn those user stories or customer requirements directly into tests.

[Principle]: Prefer tests against behavior, not implementation — Seek to test "behavior" using the language of the domain to write the tests. There will be times when the tests you need to write are actually against more technical concepts (ie: integration tests), but you can always write your tests to test behavior, not implementation.

As for knowing how to test behavior— that's a different story.

Most of the time, the code we write is more complex than plain ol' vanilla JavaScript or TypeScript. Typically, we're writing code that relies on dependencies like web servers, caches, databases, and even front-end library code like React or Vue.js 3.

It takes some up-front planning and foresight to figure out how we're going to test our code in most of these scenarios.

We leave testing until the very end

I also think that testing is hard because developers often write tests after the production code has been written. While this approach sometimes works, I don't think it's the best way to go about things.

Writing all the tests at the end isn't really acting like we value feedback, because we're leaving all the uncertainty of "if we can even test this thing", "if this thing was designed well", and "if this thing even works" to the very end.

Testing is a part of architecture

This has been a massive realization for me. And I hope it will be for you as well. Testing is a part of architecture.

Ralph Johnson, co-author of the famous design patterns book said this of architecture:

"... it is the decisions you wish you could get right early in a project”

Architecture is about the expensive, hard to change stuff like choosing a tech stack (React, Apollo, GraphQL, Mongo), an architectural style (Reactive, Event-Driven, Transaction Script), or in this case — our testing approach.

The trouble with not thinking about how we're going to test early on is that we're leaving a lot of room for uncertainty down the road towards the end of the project.

We're not sure if there are edge cases we're missing, if there are structural problems with the way we've written our code, and if we're even going to be able to test the thing.

Stress level with tdd

Your stress level over time when you realize code is hard to deploy and test towards the end of the project.

[Principle]: Expose uncertainty early - Decide on how you're going to test and deploy your application at the start of the project. Get your test architecture set up in Sprint 0.

As Agile software developers, we should value feedback, exposing bad design and uncertainty as early as possible.

"I'm not a great programmer, I'm just a good programmer with great habits" — Kent Beck, the creator of Extreme Programming


At this point, I hope you're sold on what tests can do for us and why we'd want them.

Now let's talk about the TDD process.

The Test-Driven Development (TDD) Process

TDD (test-driven development), is a technique — or a process for developing software. The goal is to keep code quality high and keep you productive, even as projects grow to be really large and complex.

Red-Green-Refactor

The TDD process works by following the Red-Green-Refactor loop. It goes:

  • Red — Write a failing test
  • Green — Write just enough code that will pass the failing test
  • Refactor — Criticize the design and refactor the code, keeping the tests intact

Red-Green-Refactor TDD

We should like this process because it keeps tight feedback loops. It gives us the ability to produce cleaner, simpler designs and helps us introduce abstractions only when they are absolutely necessary (see YAGNI — You Aren't Gonna Need It).

Should I always follow the TDD loop?: I know what you're thinking. Khalil, you can't expect me to completely follow this rule. You probably don't even do this. You're right. I don't always follow it. Rules are a great way to get started, but sometimes I break them. When I'm driving, I'll sometimes perform a rolling stop instead of coming to a complete stop. When I'm crossing the street, sometimes I'll take a quick gander to my left and right before jaywalking. I'm of the mindset that if you master this technique, you'll have the skill to decide when to break it, and to do so with confidence.

Types of tests (unit, integration, E2E, acceptance)

There are different types of tests, and you can apply the Red-Green-Refactor process to each of them. As we mentioned earlier, the scopes of these tests are up for interpretation, and they may be slightly different if you're a front-end or back-end developer, but in general, they are:

  • Unit ⭐ — Test an individual, isolated component
  • Integration ⭐ ⭐ ⭐ ⭐ — Test that multiple units work together OR "tests that confirm our code works against code we don't own" (like external APIs, databases, caches, etc)
  • End-to-End ⭐ ⭐ — Tests that act as a user actually using the application; tests the entire stack from top-to-bottom
  • Acceptance ⭐ ⭐ ⭐ ⭐ ⭐ — Domain-driven tests that verify a user story (also comparable to a use case, customer test, command/query, feature, or vertical slice) works as expected.

The big picture: How TDD works on real-life projects

Allow me to give you the big-picture so you can see where we're going with TDD and why I find it so incredibly powerful.

In the real-world, a good testing architecture typically involves more than one type of test.

tdd-in-extreme-programming

In Extreme Programming, tests are a mandatory part of planning and feedback loops.

As written about in the influential "Extreme Programming Explained" and "Growing Object-Oriented Software Guided By Tests" books, the test we start with is the acceptance test: that is — the test that most closely represents the feature we want to build. From here, we build out the internals of the feature using other tests including unit tests.

Starting with the acceptance test

Starting with the acceptance test, we convert the user story into a behavioral test written using the exact same language from the domain. This means our tests should read like plain English, and represents the exact user story that we're about to build.

Acceptance tests read like domain-driven customer requirements. These are the user stories, commands/queries, use cases, and so on.

From here, we identify the objects and their public APIs (properties, methods, etc) for the feature we need to realize while maintaining a single layer of abstraction. When we need to, we step a layer deeper into the objects we come up with and write more specific tests.

This technique is called Double Loop TDD and it's how we sculpt our solution to fit our tests.

Double Loop TDD

The idea of Double Loop TDD is to maintain two TDD loops.

The outer loop:

  • is the acceptance test loop
  • and it catches regressions

The inner loop:

  • is the unit test loop
  • and it measures progress towards implementing a feature

When we're writing tests for the outside acceptance test loop, we're usually coding from the outside-in. When we're writing tests for the inner unit test loop, we're coding inside-out.

Inside-Out and Outside-In. These are also names for two schools of thought for TDD.

Classic (Inside-Out/Chicago) and Mockist (Outside-In/London) TDD

Classic TDD (also known as Inside Out or Chicago style TDD) is a style of unit testing where we start from the unit tests, and build outwards, fleshing out the internal details of the objects we know we need. This is probably the most straightforward and commonly known form of TDD.

Whereas with Mockist TDD (also known as Outside In or London style TDD), we start at the boundaries of the application (typically the controller) and end up coding inwards, creating mock objects to build out the shape of the system and the APIs of objects we know we're going to need to fulfill the acceptance test.

Both styles of TDD are useful. By using a mixture of both inside-out and outside-in, we can compose an elegant approach to TDD our way through an entire project.

[Principle]: Inside-Out when you know how to build it, and Outside-In when you're working out the pieces.


We've covered about 70% of the essential theory for TDD so far. If your head is spinning, that's OK. One of the goals in this introduction is to show you the big picture of TDD and how it's used to develop software from start to finish.

Next, you'll want to get the technique down.

Let's walk through a demonstration.

Reminder: If you prefer to see the demonstration visually, you can watch me code it here.

Demonstration: Classic TDD — Palindrome Example

We'll build a palindrome checker using the Classic TDD approach.

Requirements

  • Create a palindrome checker
  • It should be able to detect that a string is a palindrome: that is, it is the same word or phrase in reverse.
  • Words like "mom" are palindromes
  • Words like "bill" aren't palindromes
  • It should still know that something is a palindrome, even if the casing is off. So that means that "Mom" is still a palindrome.
  • It should also be able to detect palindromes in phrases like "Was It A Rat I Saw" and "Never Odd or Even" too.

Rules

So we know we're going Red-Green-Refactor. According to Robert. C Martin, the three rules of TDD for this are:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test

Getting started

I recommend trying this example as well using the Simple TypeScript Starter repo. To get started, paste this command in your terminal.

git clone https://github.com/stemmlerjs/simple-typescript-starter.git
cd simple-typescript-starter
npm install && npm run test:dev

1. Write the failing test

The first test I'll write is that the palindrome checker can tell that "mom" is a palindrome. I will:

  • Write the test name using the requirements
  • Pretend that something called palindromeChecker exists and that it has an isAPalindrome method on it.
  • Expect the method to return true for mom.
  • Save

index.spec.ts

describe('palindrome checker', () => {
 
  it('should be able to tell that "mom" is a palindrome', () => {
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ❌   });
  
})

At this point, our test should be failing because nothing named palindromeChecker exists and it doesn't have an isAPalindromeMethod.

Red-Green-Refactor

2. Write the simplest code to make the test pass

Let's move to green. In this next step, we:

  • Create a PalindromeChecker class
  • Give it an isAPalindrome method
  • Return true (the simplest thing that would work)
  • and import it in our test

Implementing this, the index.ts containing our PalindromeChecker looks like this:

index.ts

export class PalindromeChecker {
  isAPalindrome (str: string): boolean {
    return true; // This is the simplest thing!
  }
}

And our test file now looks like this:

index.spec.ts

import { PalindromeChecker } from './index'
describe('palindrome checker', () => {
 
	it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker();    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅   });
  
})

Our test passes 🎉.

Red-Green-Refactor Green

Let's move to the next step.

3. Refactor

When refactoring, keep a lookout for duplication (at least three times) and code smells.

Red-Green-Refactor

However... There's none here yet, so let's move to the next failing test.

4. The next failing test

Let's grab the next test. We're going to test that "bill" isn't a palindrome. Our tests should look something like this now.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {	  const palindromeChecker = new PalindromeChecker();    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ❌   });})

Since our test fails, we're in the red phase.

Red-Green-Refactor

Let's turn it green.

5. Write the simplest code to make the test pass

Currently, this is what our PalindromeChecker class looks like:

index.ts

export class PalindromeChecker {
  isAPalindrome (str: string): boolean {
    return true; // This is the simplest thing!
  }
}

There are several ways to go from Red to Green. If we're really writing the simplest possible thing that would work, then we'd likely find no simpler thing to do than this:

index.ts

export class PalindromeChecker {
  isAPalindrome (str: string): boolean {
    if (str === 'mom') {
      return true;
   } else {
      return false;
   }
  }
}

But we know where this leads — so let's implement the Obvious Implementation of reversing the string and seeing if it matches the originally provided one.

index.ts

export class PalindromeChecker {
  isAPalindrome (str: string): boolean {
    const reversed = str.split("").reverse().join("");
	  return reversed === str;
  }
}

And if we save that, we should notice that our test passes.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
		const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ✅ 
  });
})

Nice — now we're in Green again.

red-green-refactor-green

6. Refactor

Refactoring doesn't just mean refactoring production code. It means refactoring our test code too. And you may have noticed that there's some duplication in our test setup.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker();    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
	const palindromeChecker = new PalindromeChecker();    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ✅ 
  });
})

However, we're going to use the Rule of Three to remove duplication. So we'll leave it for now.

[Principle]: Use the Rule of Three to remove duplication —Introducing abstractions too early is one cause of poor design. Let's wait until we see duplication three times before we make an effort to refactor it.

7. The next failing test

The last test I'll demonstrate is the test case that specifies that a word is still a palindrome, even if it includes capitals and lowercase letters. The business rule here dictates that a word is a palindrome regardless of if it is capitalized or not.

Let's write the failing test.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker(); 
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
		const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ✅ 
  });

  it('should still detect a palindrome even if the casing is off', () => {    const palindromeChecker = new PalindromeChecker();    expect(palindromeChecker.isAPalindrome("Mom")).toBeTruthy(); // ❌   });})

Aha! Look. We have that duplication we were talking about a moment ago. But we're not in the refactor phase yet. We're about to turn this Red test Green.

Red-Green-Refactor

We impose a little bit of discipline here and shift our focus to making this test pass.

8. Write the simplest code to make the test pass

At first glance, the simplest thing to do is to merely add .toLowerCase() to both the original and the reversed strings. So let's do that.

index.ts

export class PalindromeChecker {
  isAPalindrome (str: string): boolean {
    const reversed = str.split("").reverse().join("");	return reversed.toLowerCase() === str.toLowerCase();  }
}

And if we save it and check our tests, we should see it pass as well.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  it('should be able to tell that "mom" is a palindrome', () => {
    const palindromeChecker = new PalindromeChecker(); 
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
		const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ✅ 
  });

  it('should still detect a palindrome even if the casing is off', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome("Mom")).toBeTruthy(); // ✅ 
  });
})

Red-Green-Refactor

9. Refactor

And now that we're in the refactor phase, we can look to improving that duplication we saw earlier.

Red-Green-Refactor

Jest, our test runner, has a way to specify things that we must do before each test. And creating the PalindromeChecker is what we'd like to do here.

We can clean up our code with the following:

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {

  let palindromeChecker: PalindromeChecker;
  beforeEach(() => {    palindromeChecker = new PalindromeChecker();  })
  it('should be able to tell that "mom" is a palindrome', () => {
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy(); // ✅ 
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy(); // ✅ 
  });

  it('should still detect a palindrome even if the casing is off', () => {
    expect(palindromeChecker.isAPalindrome("Mom")).toBeTruthy(); // ✅ 
  });
})

That looks good to me!

Continue

We can continue on like this by writing more and more failing tests, making the code more generic to make them pass, and improving the design.

I'll leave the next two tests for you to do.

index.spec.ts

import { PalindromeChecker } from './index'

describe('palindrome checker', () => {
  let palindromeChecker: PalindromeChecker;

  beforeEach(() => {
    palindromeChecker = new PalindromeChecker();
  })

  it('should be able to tell that "mom" is a palindrome', () => {
    expect(palindromeChecker.isAPalindrome('mom')).toBeTruthy();
  });

  it('should be able to tell that "bill" isnt a palindrome', () => {
    expect(palindromeChecker.isAPalindrome('bill')).toBeFalsy();
  });

  it('should still detect a palindrome even if the casing is off', () => {
    expect(palindromeChecker.isAPalindrome("Mom")).toBeTruthy();
  });

  it('should be able to tell that "Was It A Rat I Saw" is a palindrome', () => {
    // You do this one!
  });

  it('should be able to tell that "Never Odd or Even" is palindrome', () => {
    // And this one!
  })
})

Conclusion

We've covered a lot of ground here.

We discussed why we want to write tests, what TDD is and why it's important, what it looks like in the real world, and we demonstrated the basic technique: implementing the Red-Green-Refactor loop using Classic TDD.

Homework

  • Master the Classic TDD technique. This should feel like breathing before we jump into Outside-In TDD and combine 'em to do Double Loop TDD. Try the Red-Green-Refactor process out on more coding katas.
  • Try this out when you're building your domain objects (like value objects and entities).

Next in the series


You also likely have questions at this point:

  • "how do you write acceptance tests on the front-end"?
  • "what is the difference between an acceptance test and an integration test"?

In that case, leave a question in the comments down below or let me know what you'd specifically like to see.

To see where we're going next with this series, check out this twitter thread and take a look at the new outline for solidbook.io.


[1]: Some developers see programming as an art, math, science, etc. I think there are really good arguments for all of these ways to look at programming, but here's my way of thinking. If you're being paid to write code to make money, save money, or protect revenue, then you should treat the work you do as a craft, because the negative economic consequences for doing it poorly far contrast those of programming for exploratory, experimental, philosophical, or creative reasons.

[2]: There are two broad-stroke categories of test: white box and black box. White box testing is more concerned with what's on the inside. You could say these are tests for the developer. Black box tests are more concerned with the observable output and less about the internals or what's going on within the code. You could call these tests for the customer. When we refer to testable code, we're referring to the mean between these two extremes. If your application adhered to CQS and a query doesn't change the system, then you could technically black box test any badly written code from the outside. It is significantly more difficult (maybe even sometimes impossible) to successfully white box test badly written code. This is why it's a good idea to treat tests as architecture - to start with it before writing any code.

[3]: Architecturally speaking, there is core code and there is infrastructure code. Core code is the code that you use your hands to write. On the backend, with respect to the clean or hexagonal architecture, this is application and domain-layer code. On the front-end, with respect to client-side architectural layers, this is the interaction layer and UI logic. It's the code that you own. Infrastructure code is the stuff that you don't own. Databases, caches, and web servers on the backend and view layer libraries, GraphQL clients, browser APIs and HTTP clients on the front-end. In the real-world, we use both. And this is where many of our testing challenges arise. How do we decouple our core code from the infrastructure code so that we can test our code, and test it fast? These are questions that are better asked and answered earlier than later on in a project.



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


8 Comments

Submit
Emilio
8 months ago

Nice explanation Khalil

Steve
8 months ago

TDD is awesome :)

Eugene
8 months ago

Awesome, thanks. Didn't know about inside/outside styles of TDD development

Steven
8 months ago

Nice Article !!

Dawid Janiga
7 months ago

Great introduction Khalil, thank you

Khalil Stemmler
7 months ago

No problem David! I saw the rest of your message in the backend btw - it looks like there's an issue with emojis.


"I would be more than happy to see answers to those questions

  • how do you write acceptance tests on the front-end?
  • what is the difference between an acceptance test and an integration test

... as the example of React application."


To that, I say - coming right up! Thanks!

Daniel Ochoa
7 months ago

Thank you so much! Very interesting article, you explained it very clearly and in a fun way.

Kauan Mocelin
4 months ago

Best explanation step by step about TDD I have readed! Very nice!

saud chougle
3 months ago

how to write tests that manipulate dom?


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Test-Driven Development



You may also enjoy...

A few more related articles

How to Test Code Coupled to APIs or Databases
In the real-world, there's more to test than pure functions and React components. We have entire bodies of code that rely on datab...
How to Mock without Providing an Implementation in TypeScript
Having to provide an implementation everytime you create a test double leads to brittle tests. In this post, we learn how to creat...
When to Use Mocks: Use Case Tests
Mocking gets a pretty bad rap. However, if you're building an application using object-oriented programming and you're making use ...
Why I Recommend a Feature-Driven Approach to Software Design
Features represent the essential complexity of software design. It's the complexity that can't be avoided. Everything else — the l...

Want to be notified when new content comes out?

Join 10000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates