How Frontend Tests Became the Unsung Heroes of Our Project 

The importance of application testing

While most programmers are already familiar with the best practices of testing their code, they often focus more on backend testing which leaves frontend testing overlooked. In this blog post I will explain how taking time to learn and implement frontend tests helped us save time and stick to our deadline while keeping our code bug-free. 

 

It is very important to test all of the application logic but when it comes to testing what the user sees, most of us rely only on manual testing. This can be a good approach when working with smaller applications and when the application’s functionalities are straightforward (without many special cases). Also, given that most applications start smaller, it is only necessary to write frontend tests at the beginning of the project if you already know that they will get more complex with time. 

 

Manual testing becomes much more time-consuming with more elaborate applications since each new functionality requires regression testing. This is because each new functionality could reintroduce an already-fixed bug. Unless you can track all the connected logic and retest old bugs, there is always a possibility that some part of the application will not work correctly. 

 

Now that you are aware of the importance of frontend tests let me tell you about our project and how we decided on implementing frontend tests.

What motivated us to start with frontend tests

The application at the time was developed in React but had a problem with bugs because it was done quickly and by only one person. So, since we were more familiar with Angular and the client agreed, we decided to switch to Angular. 

 

This project was unique because we only had to implement the frontend but not the backend. Because of this, the team consisted of people who were more oriented towards frontend development and who were eager to learn new things.

 

So, the goal was to create a new, more stable application and we wanted to make sure that it was as bug-free as possible. That’s when we came up with the idea of writing frontend tests from the very beginning. 

In the beginning, our development was slow because we were still learning how to write the tests. We practiced mob programming (when the whole team codes together and works on the same task) so that we were all familiar with the whole code base and also to get used to writing tests. But even though we were slow, this set us up for future success since we covered every new feature with an appropriate test. You will soon see how that impacted our whole project but first let’s check how these tests were written.

The implementation of frontend testing in our project

The project I was working on used two different types of frontend tests: unit and e2e (end-to-end) and we were quite happy with the coverage that it provided us. There are also other frontend test types that you could consider like integration, accessibility, and cross browser. But since we were just starting with frontend tests we went with unit and e2e tests. 

 

Unit tests are performed on smaller parts of code and are done in isolation by mocking all the dependencies. For our project, we used Jest as a testing framework for unit tests and we wrote tests for parts of components like error messages. In the code block below you can see an example of a jest test for our registration form. In the test, we are checking that an error message appears when the user does not accept the privacy policy.

it('termsAndConditionsNotChecked_showsTermsAndConditionsErrorMessages', async () => {
 const user = userEvent.setup();

 await render(RegistrationFormComponent, renderOptions);

 const submitRegistrationButton = screen.getByTestId(
   'submitRegistrationButton',
 );
 await user.click(submitRegistrationButton);

 expect(
   screen.getByText('You must accept the privacy policy'),
 ).toBeVisible();
});
Copy

Since unit tests run quickly we could test all the different errors that could appear like dynamic messages for password fields that would show which rules are still not met. Other than error messages, Jest tests were used to test pipes, route guards, and util classes. Having all this covered, our e2e test could be more concise and concentrate only on vital parts of the application flow. Also since e2e tests are slower we saved time by testing edge cases and messages separately in unit tests. 

End-to-end (e2e) tests covered all possible user flows inside the application. This is not necessary for all projects but when our project started we had time since there was no set deadline and we decided to cover everything. Most teams do not have the same conditions and if you do not have much time don’t completely give up the idea of writing frontend tests. Instead, concentrate on covering all the crucial flows. That way you still might not catch all bugs but you will be certain that the most important parts are working as expected. 

 

We used Cypress for e2e tests as it was quite simple to use and we got a hold of it quickly. This kind of testing best simulates a real user because most of the tests are written by fetching elements by text and doing actions on them or asserting that certain text is visible. 

The test provided below is checking if the update of the first name is working. It is important to note that since this is an e2e test we will usually have some setup before the actual logic that we are testing. In this example, the user first has to be created and logged in and for this, we created commands since the same setup code was used in many tests. After login, the user navigates to the profile page and changes his first name. After that the test checks if the first name was actually changed and the new name is visible on the page. 

it('Edit firstname updates header display name', () => {
 const email = 'test@email.com';
 const password = 'testPass';
 cy.registerFullUser(email, password);
 cy.loginUser(email, password);

 cy.visit('/profile');

 cy.contains('First name').click();
 cy.get('[data-testid="firstNameInput"]').should(haveValueChainer, firstName);
 cy.get('[data-testid="firstNameInput"]').clear().type('NewTestName');
 cy.get('Submit').click();

 cy.contains('NewTestName OldLastname').should(beVisibleChainer);
});
Copy

From this example, we can see that the test simulates how a real user would use our application and make the change on his profile. So by running this test we can skip the manual testing and be certain that the edit functionality is working as it should.

The impact of our approach

So were there benefits of this very test-heavy approach?

In the meantime, our project deadline was moved up, leaving us less time to finish everything but we still made it thanks to this approach. The pace got quite fast but we kept writing new tests with each added functionality and we were actively working on the application almost to the production date. No manual testing and clicking through the application was needed to check if a new functionality caused a bug. We just ran our tests and pushed our changes to Git. 

When the production day finally came, we had very few problems and most bugs that were found were some edge cases that weren’t tested. When these bugs were fixed we also updated the tests to cover the fix so that we could be sure they wouldn’t reappear in the future and go unnoticed. Now that some time has passed since the first production date I can surely say that there were no bigger problems. This is a great result for a project with a sooner deadline than expected and we owe this largely to rigorous frontend testing.