5

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

 1 year ago
source link: https://hackernoon.com/does-your-web-application-need-end-to-end-and-unit-tests
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

February 22nd 2023 New Story
8 min
by @marcinwosinek

Marcin Wosinek

@marcinwosinek

I'm a JavaScript developer. I'm here to teach you useful...

Read this story in a terminal
šŸ–Øļø
Print this story

Too Long; Didn't Read

The name end-to-end (E2E) comes from the nature of the testing: we are checking the application from the frontend to the backend. E2E tests are meant to simulate user behavior, so the scenarios we test will be similar to what the user could do in the application.
featured image - Does Your Web Application Need End-to-End and Unit Tests?
Your browser does not support theaudio element.
Read by Dr. One (en-US)
Audio Presented by

@marcinwosinek

Marcin Wosinek

I'm a JavaScript developer. I'm here to teach you useful ...

Learn More
LEARN MORE ABOUT @MARCINWOSINEK'S EXPERTISE AND PLACE ON THE INTERNET.

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:

  • I want the application to have the highest quality possibleā€”instead of moving fast and break things, I aim to move slow and not mess up the production;
  • I plan for long termā€”I donā€™t believe in starting from scratch, and I hope the things I build will be used in 10 years and beyond; and
  • I care about developersā€™ experienceā€”I want reliable tests, with possibly a fast feedback loop for developers. Both locally and on continuous integration (CI).

So keeping this in mind, letā€™s take a look at automated quality tools.

End-to-end

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:

  1. Log in as a customer,

  2. Find a product,

  3. Add it to the cart,

  4. Go to the checkout page,

  5. Expect one product in the cart and a total equal to its price.

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:

  • Cypress
  • Playwright
  • Nightwatch
  • (deprecated) Protractor for Angular applications

Unit tests

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:

  1. Create the order object and call it testOrder,

  2. Add a product object to the order,

  3. Expect 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:

  • Jasmine
  • Mocka

Feature overlap

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?

Downsides of end-to-end

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.

Complex set up

The tests Iā€™ve mentioned require:

  • an HTTP server that hosts frontend files,

  • a backend server up and running, and

  • two databases, preset with data that the tests require to run.

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:

  • a CI server that starts jobs.

  • CI agents that run the jobā€”run in Docker containers as well.

  • for some time, we had a CI agents coordinator that started and stopped the CI agent based on demand for the CI server.

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.

Slow in execution

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:

  • it complicates the setup even more.
  • it may require some changes in tests to avoid collision if each test runner talks to the same backend.

Slow in writing

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.

Strength of unit tests

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:

  • browser,

  • application,

  • backend server, or

  • databases.

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.

Interactive specification

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.

Helps you design units

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.

What goes where

So, if I keep both unit tests and E2E, how do I decide what should be tested with which tool?

Smoke tests

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 for the user stories

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.

Painful bugs that often affect users

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.

Subtle details of implementation

There are many important yet subtle behaviors that would be very difficult to test directly from a GUI. For example:

  • Rounding prices correctly when you apply discounts.
  • Setting translation to a correct language based on a combination of browser settings, user data, cookies, etc.
  • Weird cases that should never happen in your application in the normal user session. For example, data that was saved to 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.

Everything else

If you do TDD, you are expected to test everythingā€”and the only realistic way of doing it is unit testing.

Also published here.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK