Before working on our web application at Haus, I had done some frontend work, but not really any frontend engineering. I worked primarily in APIs and libraries where I did a lot of TDD because, after years of practice, I could anticipate use cases for these interfaces and had a good workflow set up.

Working on the frontend of our application however, I found TDD difficult to do and less satisfying than with backend code. This article discusses why I think this was so hard and why you might find the same thing, especially if you come from a TDD background outside of frontend development.

Benefits of TDD

You’ve probably heard many arguments for TDD, including some studies indicating its benefits for correctness, efficiency, etc., but from a purely subjective point of view, I like TDD because:

It forces me to decompose. Tests that are hard to write often indicate poorly factored code.

It makes me think for at least a moment about the design of my code and the problem it’s trying to solve; it is a mindfulness technique.

It puts me in a position of empathy for other programmers that must use my interfaces. Even if the other programmer is me in an hour, it still forces me to become a user of my own code.

It helps me discover all the combinatoric possibilities of my code because I have to write them and read them, and read them in spec form every time my tests run.

When TDD is difficult?

I’ve found TDD difficult many times and it’s often because there’s something that makes the benefits above not achievable. I initially found TDD difficult because I had simply never done it and, as a new craft, most things got better after some practice. However, there are still many times (like the topic of this article!) where it’s still tough.

To mirror the benefits above, TDD tends to be difficult when:

I don’t have a good idea about how to decompose something. This situation often comes about when I’m learning a new pattern, programming language, or environment.

There is a time crunch and I don’t feel (often erroneously!) that I have the time to design things before jumping in headfirst.

I can’t anticipate how someone will use the code I’m about to write, which applies to both the empathy point and the combinatoric exploration.

Outside of these points, TDD is also difficult to do when you’re not unit testing or testing something that has a lot of dependencies. Having multiple, contingent or conditional steps makes it really hard to write tests first that will match good code.

The first two points above I will very much take blame for and not fault TDD, especially given my comparative inexperience with frontend coding at the beginning of our project. The rest, however, I think may be a shared problem…

Humans are hard to predict

Where will they go next?

So this brings us to the biggest problem with TDD and frontend code.

Users behave differently than both code and API consumers when using UI and you will not be able to guess how

As I mentioned above, I have experience building two or more components, usually backend API or libraries, that use each other. I can even empathize with human programmers who will use these interfaces and I can design something that I think most folks will be able to use.

But when the other component is not code or even a programmer engaged in the act of programming, all bets go out the window. When I tried practicing TDD with user interfaces, I would make sure that the code and interface acted exactly as I would think to use them and as soon as I handed them to someone else to play with, I would be wrong about something and have to burn down a lot of what I had built.

Sorry.

In other words, when we cannot build both sides of the puzzle or empathize with a use case like the ones we have every day (coding to an API), TDD is totally possible, but the cycle of build up and tear down can be incredibly demotivating. What you’re feeling here is a natural result of the stage of the produce cycle that you find yourself in when doing frontend development.

User interfaces, especially compared with APIs, often tend to be used less and don’t live very long. There’s a reason we have A/B testing for user features, but versions for APIs. When dealing with code that has a higher likelihood of going away, the tests will too.

I should note that while not all the interfaces we have in our product were designed with user flows, many were, and by a proper designer. As awesome as designers are, even they cannot anticipate all the ways users interact with the product.

Our product is focused on solving a problem with some inherently complexity, so it has a complex UI. The great designers we’ve worked with use early user feedback and iteration as part of their process, so we end up responding with design and even interaction changes. By the way, we’re currently looking for a designer to help us out, so if you or someone you know is interested, check out the job description!

As an aside, folks who are just getting started in coding and/or not used to TDD who don’t understand where it shines often don’t see the point of using it. I recommend trying to write some pure logic components or APIs to see TDD’s best side because it’s fantastic for that case.

What is the frontend and how do we test it then?

So up to this point, I’ve used the word frontend to describe a thing that’s hard to TDD. If you’re a web developer though, do not think that everything you have running in your user’s browser is hard to test or is even frontend code.

Not everything running in the browser is hard to test or even frontend code

Because a lot of business logic running in a browser or other client is so close to the UI, you may get the impression that it’s all hard to use TDD with. That’s certainly not the case and going back to the first benefit of decomposition, you should be trying to extract as much business logic from your UI and put it into pure functions, tested via TDD as possible. React has emerged with some great practices to this end, Flux-like architectures are great for it by design, and I’m sure most other frameworks/libraries have useful practices as well.

Snapshot/regression tests

One of the techniques that has emerged with React code, especially using Jest, is the idea of snapshotting a successful state and making sure that components reproduce that state. This approach is pretty much the exact opposite of TDD, but it’s incredibly valuable in that it helps you make sure you’re not breaking anything as you make changes.

All subsequent runs are compared to the original.

TDD produces that benefit as well, but you pay an upfront cost to make sure the tests work and that you’re getting what you want. With snapshotting, you’re manually verifying that you’re getting the behavior you want. You can also do your snapshotting, try to change the component to produce something different, and make sure they break. With this approach, you get GREEN -> RED -> GREEN, which at least makes me feel a bit better that the snapshotting will catch regressions when they happen.

Tell me I’m wrong and show me the way

I love practicing TDD. I would love to be wrong about this, so please share your experiences below about how you have figured out how to do TDD with user interfaces that doesn’t end up with throwing away most code immediately.

If you shared my experience, I’m happy to hear about that too, if only for reassurance. Win/win! 😄