DEV Community

loading...
Cover image for The religion of test-driven development

The religion of test-driven development

frontendphil profile image Philipp Giese Originally published at philgiese.com on ・7 min read

I recently talked to another developer and mentioned that I use TDD. The other person then asked me whether I'm also one of that religious TDD followers? I had never thought about this. Do people think I'm preaching to them when I encourage them to use TDD? Are they only doing it to not hurt my feelings because they think I'm a believer? Did I create an atmosphere where TDD is a dogma that cannot be questioned?

Let's talk about TDD, why it's not a religion, and why I think it's so much more than a practice to write code.

TL;DR

If you're treating TDD like a religion you should probably reconsider your life choices. Because following something based on dogma won't do you any good. This article focuses on:

Why TDD is no religion

Just because.Because you shouldn't blindly follow any practice to write code. If you think you need to preach anything work-related please stop. Life is too short to get into fights about whether yours or someone else approach is better. You're probably both wrong. At least to some degree. There simply is no perfect, one-fits-all solution to anything out there.

Keep your mind open. Assume that no one gets up in the morning to do a bad job. Be mindful and your stress level will decrease a lot.

Why I write tests first

If you don't like my reasons then you might want to read what Kent Beck has to say on the matter. This section is purely my personal opinion. If you have strong feelings about any of the claims I make I encourage you to reach out to me (find out how in the last paragraph). I'm more than interested to hear your thoughts.

Create APIs from the perspective of the consumer

When you start with nothing you can free yourself from thinking about the details. I know that the test will be failing in the beginning. Even more so, I want the test to fail initially. When I create an API that does not exist yet I can dream up anything I want. By doing this I can make sure that the DX is great. Because when you start from the other side you often end up with APIs that leak details about your implementation. However, when there is no implementation yet you cannot leak anything. This can help you to build minimal APIs that are easier to consume.

Keep me from doing too much

When I start on a new and exciting topic I can get ahead of myself. If I directly start coding and already have a couple of use cases in my head I can end up in a situation where there is a lot of code and the borders between the use cases start to get blurry. Sometimes they even start to influence each other. Tests help me to stick to one use case because I can follow some simple rules. When I write a test first I know that when the test goes from red to green I'm done. If I think that I'm not done yet this means that I need to add another test case. Tests help me split a large task into small units of work.

Make sure the test points to the correct problem

This is one, if not the most important reason. When I write the test first I'm forced to see it fail. I then need to verify that it fails for the correct reason. While doing this I not only make sure that the test targets the correct problem but also that the error message is descriptive enough so that others in the future have enough context should their changes break the test.

You might lose all that when you write your tests after you added the implementation. I've seen it too often that developers tend to write tests that replay what they have implemented. That does not prove anything. Even worse, these kinds of tests fail for any number of reasons that have nothing to do with what they should verify.

Help future me pick up the work tomorrow

Context switches suck.

Even when I continue with the exact same task the next morning I need some time to figure out where I left off and what I need to do next. What helps me a lot is to end the day with a failing test. This way past me sets a pointer for future me for where to pick up the work. I can get right into coding again and make that test green. After that first test, I'm usually back on track.

Give me serotonin rushes every couple of minutes

Each time I finish something I'm happy. Ending a day with the feeling that I haven't accomplished anything is super frustrating. When I code the whole day but can't be sure that what I've done actually works then this makes me sad. But when I TDD myself through the day I have that feeling of accomplishment every couple of minutes. Even better, I can be sure that everything I've done so far works as expected.

This feeling of getting sh*t done and the serotonin rush that follows is probably also the cause why I got addicted to TDD. Right now it's more like a drug (in the most positive way) than anything else.

TDD is much more than writing tests

Here's the kicker. For me, the biggest advantage of TDD is not the tests.I see TDD as a great forcing function to help developers solve the right problems.

When a developer sits down starts with a test she needs to understand the problem first. I would claim that it's close to impossible to write an assertion when you don't know the problem you're trying to solve. At least, it will become much harder to that. If our developer realizes that she lacks some information this happens before she starts building the new functionality.

I'd like to point out that the most important aspect here is that all this happens at the start of the task. This means:

  • She hasn't spend days writing code she might need to delete later
  • She is not frustrated because she has not invested much time yet
  • The person who helps her will not need to understand any implementation details

Not having a possibly wrong implementation frees you from a whole set of issues as well. Without code there you will not have to rework anything. Also, you can't get attached to anything you've already built and try to justify keeping it in regardless.

If you figure out that the problem is not entirely clear then you can clarify it. That's also great because there cannot be blame at this point. Why? Because when the resources you work with aren't enough for you to figure out the problem then it cannot be your fault. At some point, someone has probably made some assumptions and you merely discovered these shortcuts. By asking for clarification you are the hero in this story because you are now making sure that you're working on the right thing.

My personal experience working like this is that it improves the atmosphere a lot. It's easy to get mad at someone for spending days of work but not solving the actual problem. I have caught myself asking Why didn't you ask me earlier?. But then I also realized that the person didn't realize that he was going in the wrong direction. With TDD in place, people stop to ask for help and start to ask for clarification. That's much nicer. Because then the person who asks doesn't try to undo a mistake but actively seeks out clarification to avoid doing that. In my experience, this leads to solely positive reactions.

To end this post on an even more meta-level I'd like to propose a name change. I believe that by working with TDD we will end up with something that I would call CDD or communication driven development. Because this is what I believe is what helps development teams the most. To talk to each other and to do this as early and as often as possible. We talk a lot about feedback loops. But I think people, especially developers tend to think of feedback as something that comes from customers or managers and that means more work. Making sure you're working on the right problem is another form of feedback that is cheaper to get and will already help us a great deal.

What is your experience with TDD?Do you use it for your company? If yes, would you agree with my observations or not? If no, does this article encourage you to try it out? Tweet @philgiese with your ideas and questions.

Discussion (4)

pic
Editor guide
Collapse
0916dhkim profile image
Donghyeon Kim

Thanks for the article. Have you tried TDD in frontend development? I'm working on a React project right now, and sometimes it gets difficult to determine what to test. Also, splitting the UI into independent units is not always trivial. I would like to get some advice for writing testable frontend code.

Collapse
frontendphil profile image
Philipp Giese Author • Edited

Hey there! In fact, I'm mostly working with client code. Testing library is a great tool to write tests for frontend code. It's designed in a way that makes you "use" your app much like your users would. This helps you to avoid testing implementation details. That in turn helps you write fewer brittle tests.

I developed a habit to care about component decomposition at the latest state. I start with a test and build out a component. Once it becomes too large I split out smaller components. The great thing is that I don't necessarily have to move the tests into new test suites. Sometimes they only make sense in a larger setup. I've gotten used to thinking about UI tests as integration tests only. Unit testing UI components might sometimes make sense. However, if you start to mock everything that is interesting what are you actually testing?

Collapse
0916dhkim profile image
Donghyeon Kim

It makes better sense now. I, too, mostly use Testing Library with Jest, but my problem has been that I always tried to make my tests smaller. Let me try writing more integration tests than unit tests and see how it goes. Unlike your workflow, I usually create a small independent component with its unit tests and put the new component inside existing components. I guess my tendency to write smaller tests made them more "brittle."

Anyway, here are two objectives I think about when I write my tests:

  1. Does the component reflect the state correctly?
  2. Do user interactions have correct consequences?

1 is easy to test, but it can be difficult to setup when the state is external (not props) or complicated. I mock external data.

2 is more tricky to test IMO. Many side-effects must be mocked (e.g. data fetching & url redirection). Also, querying for HTML elements is not trivial; I often end up with parent <div> when I actually wanted to select the child <input>. Querying is difficult for me because I am writing my tests for components which do not exist yet. Today, I wrote a test for a React form that goes like this: "fill out two text fields, select an option from a dropdown, press a button then check if the form sends a correct request." I spent more time troubleshooting getByRole() calls and fireEvent calls than implementing the form.

What I love about TDD is the confidence it gives me during development process. Before implementation, I have a firm objective to aim for, and after implementation, I am assured that my implementation is tested; however, writing solid tests has been a real challenge for me when it comes to frontend. Maybe it will be alright after I get more used to ARIA roles and web in general.

Thank you for your input. Now I feel like I have a better strategy to try out.

Thread Thread
frontendphil profile image
Philipp Giese Author

Glad I could help. Maybe look at it like this. If it's hard for you to write a test then it will also be hard for people with a screen reader to navigate your app. This motivated me a lot to get more comfortable with accessibility.

What you're describing looks like this to me:

const { getByLabelText, getByRole } = render(<Form />)

fireEvent.change(getByLabelText('Text input'), { 
  target: { 
    value: 'input value' 
  }
})

fireEvent.focus(getByLabelText('Dropdown'))

fireEvent.click(getByRole('option', { 
  name: 'The option you want to select' 
}))
Enter fullscreen mode Exit fullscreen mode

Accessing the request is a tricky thing. However, we can make this more accessible in our tests as well. For instance, we've created a custom render method that fits our codebase. So when we use our custom render we can do more things:

const { getFetchRequest, getByRole } = customRender(<Form />)

fireEvent.click(getByRole('button', {
  name: 'Submit'
})

const request = getFetchRequest(SUBMIT_REGISTRATION)

expect(request.body).toHaveProperty('emailAddress', 'foo@bar.com')
Enter fullscreen mode Exit fullscreen mode

We've taken what is complicated (getting the request) and turned it into a feature of our custom render wrapper so that developers can write tests more easily.

This can be true for a lot of things. redux, session handling, theming, etc... If something is very common in your app there is no rule that should keep you from extending your testing utilities.