Does My Web Application Need End-to-End and Unit Tests?

I'm JS developer with 13 years of professional experience. I'm always happy to teach my craft.
Search for a command to run...

I'm JS developer with 13 years of professional experience. I'm always happy to teach my craft.
Thanks for this write-up! The article describes well the up- and downsides of browser-based E2E tests on the one side and unit tests on the other side.
However, it would be wrong to assume that these are the only types of automatic tests.
Another one type that comes into my mind are tests that I'd call API-E2E tests: They for example call an API endpoint of the backend (dev or stage server) and check for its response.
Or, in case of a library that is to be tested, they call public (main) methods and check for the return values. Those come probably very close to the advantages of unit tests.
With API-E2E tests, you can have the advantages of both worlds:
Especially the latter makes me skeptical about unit tests because they basically force you to double your code: You not only have to write a new function but also a test for a new function.
Or if you want to rename a function, you need to do it at least twice: in both the test and the code (unless your IDE is smart enough to recognize the matching test)
Yes, I do understand that this extra code is meant to be a feature, not a bug, as it automatically gives you a specification and documentation – yet even you seem to agree that this slows down development ("instead of moving fast and break things, I aim to move slow ...").
Further remarks:
I maintain a suite of 3200 unit tests that run in about 30 seconds—only about 1/100 of a second for a test.
True, but when comparing the number of unit tests with the number of E2E tests, we have to remember that 1 E2E tests usually covers a lot of methods, whereas a unit tests usually covers only one method.
Thanks for your response! It's excellent food for thoughts for me on how to argue in favor or unit test.
You are right, "API e2e" can be a perfect tool in numerous instances. My work in 95% on the frontend, and I tend to forget about more backend focused approaches. A few years ago, I was heavily investigating testing JS tools for testing API—as a frontend dev you are doing QA for the backend anyway, so a bit of automatization can help here.
I think a big difference between our projects is that in my case I have in mind:
For me, those are the reasons to spend extra time writing tests now.
That being said, I'm not sure about the cost of doing TDD. My off hat evaluation is that over the span of a few hours, writing tests slows me at max 25%; buy over a few days benefits compound, and I'm more efficient with unit tests than without them.
One E2E covers a lot, but it makes the error messages less helpful—it tells you some part a long chain of operations is off, and you have to investigate which one it is.
I'm slowly getting to the conclusion that unit test value for solo developers is in unblocking TDD; and the design impact I've talked about in : https://how-to.dev/how-to-keep-your-units-testable-in-javascript
I’ve been working in programming for the last 16 years. Let’s see what changes I see in our industry that are enabled by generative AI. Rapid prototyping The last few months, I’ve been working on a set of WordPress plugins, related to event organizat...
Writing unit tests takes time and effort. Nonetheless, many teams insist on writing them anyway—that’s because of the benefits they bring to a project. Those benefits are mainly the following: fast feedback—unit tests speed up each iteration of twea...

Pure functions are the perfect case for unit testing. For a given input, we always expect the same output—there is no internal state involved. Let’s take a look at a few examples and some simple tests that check if the methods work as expected. Jasmi...

Let’s say you have a job interview in a few days. How should you prepare for it so that you can make an informed decision about joining the company, as well as make sure that your interests are taken care of? Prepare your questions An interview is a ...

Creating example projects is a common way of showing your skills to potential employers. Let’s take a look at what’s important to keep in mind when building personal projects with an eye toward impressing prospective employers. Simplicity Building ap...

As are most questions worth asking, the answer is nuanced and could be boiled down to it depends. I’ll show you my way of thinking and where it’s coming from. Your goals and constraints are likely different, so the final conclusions can differ as well.
My basic assumptions are:
So keeping this in mind, let’s take a look at automated quality tools.
The name end-to-end (E2E) comes from the nature of the testing: we are checking the application from the frontend to the backend. The tests are interacting with the application through the graphical user interface (GUI)—so we are testing the frontend as it runs in the browser. At the same time, we have the server and database running on the backend—through interactions in the browser, we can test whether the backend behaves the way we expect.
E2E tests are meant to simulate user behavior, so the scenarios we test with them will be similar to what the user could do in the application. An example scenario for an online shop:
Because the interface you use for interacting with the application is a graphical interface built for the user, your application is always testable this way. At most, you will need to add some attributes to help identify elements of the interface you want to interact with. The complexity of the tests depends mostly on the complexity of the workflows you have in the application.
Example libraries that allow you to create E2E test:
Unit tests are so named because what we are testing is a single unit of code: a class, a function, or any type of entity defined by the framework you use. You test those units by interacting with the interface they provide, and by mocking parts of code that they use.
Unit tests test the code from the code level. Example test scenario:
testOrder,testOrder.totalPrice, testOrder.totalTax, and testOrder.totalQuantity to match expected values This type of test is closely integrated with your code. Depending on how the code is structured, the ease of writing unit tests will vary. It can range from rather easy to almost impossible. It can be especially challenging to add unit tests to existing code that was written without testing in mind.
Example libraries you can use to write unit tests in JavaScript:
In a way, those two types of testing overlap completely: in both places, set up expectations for the code and make sure they are met. Anything meaningful that happens inside your code units will eventually find its way up to the user interface—and there you can check it with E2E. Besides that, most E2E libraries allow you to mock backend calls, or even isolate parts of your code—so you can precisely recreate very subtle scenarios.
On the other hand, there are complex user interface (UI) cases that cannot be tested in a unit test. Unit tests usually check only what is returned by JS—without calculating the whole screen with HTML, CSS, and JS in place. With unit tests, you cannot test whether a button is clickable on a given screen.
So, if unit tests have those limitations while E2E can cover everything that our unit tests does, does it mean we only need E2E in our application?
There is an E2E test suite that I’m happy about—it’s relatively fast (350+ tests run in 15 minutes), stable (random failures happen about once per each five runs), and it does a good job of catching regressions. But even good E2E tests cost plenty of development time—when they are created, run and maintained. Let’s see a few causes for why it’s this way.
The tests I’ve mentioned require:
Thanks to Docker and containerization, it’s relatively easy to share the whole stack among developers and CI server.
On the top of the requirements listed above, CI introduces a few other moving parts:
In summary, to run E2E on CI, there are few layers of cloud instances: Docker containers running inside Docker containers—in short, plenty of things. Usually, you just need one thing to fail to get the whole test run to break. This creates false-positive failures, which—if they happen too often—will train the whole team to ignore failing E2E—exactly the opposite of the behavior we would like to see.
Most E2E I’ve seen in my test suite takes about 5 to 15 seconds to run. Not too bad, but even on the lower end of the range, 350 tests would take half an hour to pass while running them one after the other. Total execution time can be lowered by running tests in parallel. This again brings few downsides:
E2E and relatively slow in terms of writing too. While developing, the execution time we discussed above introduces delays in the developer’s feedback loop. Something like 5–15 seconds is not much, but those seconds add up, and it makes staying in the productive zone more difficult.
At the same time, unit tests have quite a few important strengths.
Unit tests are very fast. Because you interact directly with code, you don’t suffer delays introduced by the:
I maintain a suite of 3200 unit tests that run in about 30 seconds—only about 1/100 of a second for a test. At this speed, you can rerun relevant tests each time you change code and keep getting almost immediate feedback. This is something that helps a lot when you are doing test-driven development (TDD): when you write code only after writing a test that checks for the expected behavior.
In a way, well written unit tests become an interactive specification—one that explains how the code is supposed to work and can be run to see whether those expectations are met. Plenty of challenges in coding are due to communication issues, and most of the communication happens in writing—especially the communication between past developers who wrote the code months ago and present developers who maintain it now.
For written communication to work, you need the reading to at least happen. It’s difficult to get others to read, especially when the communication happens across time.
If you compare ‘specification in comments’:
// quantity has to be positive
if (order.quantity < 0) {
order.quantity = 0
}
To the specification in unit tests:
it(‘should reset quantity to 0 if negative’, () => {
order.setQuantity(-1);
expect(order.quantity).toEqual(0)
})
I am much more comfortable trusting a future developer to notice conflicting changes when there are tests in place. Failing tests at least force you to see what’s up there, while comments can be easily ignored. Besides that, sometimes you are uncertain whether the comments are still up-to-date.
Writing unit tests, and especially TDD, will impact the way you write code. As your units get bigger and take on more responsibility, the testing becomes exponentially more complicated. This will give you a slight but constant push towards keeping the responsibilities well distributed across various units of your code. Over time, this will add up to a visible difference in how the logic is structured. I wrote more about the impact of testing on the application architecture in another article.
So, if I keep both unit tests and E2E, how do I decide what should be tested with which tool?
Smoke tests are a crude test to see whether the application even starts. The name comes from testing hardware—if you connect your new device to power, and the smoke goes off, you don’t need to test any further. These tests are a perfect case for E2E—at a minimum, you want each of your pages to open successfully on the screen when the user tries to visit.
Happy path is when everything is in its right place, and we don’t have to deal with exceptions or errors. The stock is in place, credit card payment is accepted, and email address is valid. It’s good to have those cases covered by E2E because happy paths cover the main reason for the application to exist. Successful transactions are the reason why users go to online stores and are why the company built the application in the first place.
For each workflow that you can cover with E2E, there are hundreds of ways it can go wrong. That’s why I usually don’t dive too much into covering edge (error) cases with my E2E—that would be a lot of work. But for a subtle issue that was made through manual testing and got to production, it’s worth evaluating whether it should be tested with E2E to prevent this kind of regression from happening again. This way, you can avoid the risk of making a bad impression on your customers by making them suffer from the same issue coming back after it was fixed. And you implement additional test automatization in places that are clearly lacking coverage with manual testing.
There are many important yet subtle behaviors that would be very difficult to test directly from a GUI. For example:
localStorage with an older version of the data structure, and you wish to make sure it’s migrated and works correctly in the current version.Those cases fit perfectly in unit tests.
If you do TDD, you are expected to test everything—and the only realistic way of doing it is unit testing.
If you are interested in learning more about testing, or other topics relevant for beginner programmers, you can sign up here to get occasional updates about my new content.