DEV Community

Ivan Malov
Ivan Malov

Posted on • Edited on

Front-end testing in 2024

Hello everyone! My name is Ivan. I'm building Frontend and interfaces more than 10 years. As it happens, I've been deeply immersed in the topic of front-end application testing for the past few years. I noticed that this topic has become much more popular than it was a few years ago. This is especially noticeable in the appearance of new testing tools. Despite this, writing tests still raises a lot of questions, especially in young teams. That's why I decided to write a short article about why and when you should write tests, how to motivate yourself and your team to do it, and what you should try to use when writing tests in 2024.

Who is this guide for?

First of all, I'm writing this guide for myself. I wanted to collect all my current knowledge about testing that I know at the moment and structure it. Since I am primarily an engineer, this guide will probably be useful for all engineers. I tried to understand what tests are, then/when and what to test. Here you can find examples of testing, modern tools and useful links.

When writing tests, I am guided by several principles. First principle, don't put tests in a black box. Set up tests as early as possible, and write them along with the code. Pay special attention to tests related to bugs. A bug is the result of manual testing, the cost of which can be incredibly high.
The second principle of testing is ensuring that the software is reliable, secure, and user-friendly. A narrow focus on test coverage can divert attention from these broader, more important objectives.

In each section, you'll find a description of each type of test, when to apply it, and an example with the tools I recommend when writing tests in 2024.

This text does not claim to be the truth in the first instance. It is primarily my experience, but I will be glad to hear any criticism or thoughts on this subject.

Table Of Contents

Understanding the necessity of testing

Software testing is the process of checking the quality, functionality, and performance of a software product before launching. It involves both manual and automated methods to detect and fix bugs, guaranteeing that the software operates as intended. The primary advantages of software testing include:

  • Ensuring Code Quality and Functionality: Guarantees that each component and the application as a whole operates correctly, both in isolation and in combination with other components.
  • Early Detection of Bugs: Catches and fixes bugs early in the development cycle, saving time and resources.
  • Facilitating Refactoring and Code Enhancements: Allows for safe refactoring and introduction of new features, ensuring existing functionality is not broken.
  • Improving Code Maintainability and Documentation: Acts as documentation for the codebase, aiding in understanding and maintaining the code, especially over time or with multiple contributors.
  • Building Confidence and Trust: Increases the reliability and stability of the application, fostering trust among users, clients, and stakeholders.
  • Cost-Effectiveness: Writing tests is more economical than dealing with the repercussions of errors in the long run, leading to significant cost savings.
  • Enhanced Maintainability and Scalability: Ensures the codebase remains maintainable and scalable, particularly vital for long-term projects with numerous contributors.
  • Promoting High-Quality Code: Encourages adherence to SOLID principles and makes it challenging to write subpar code, directly benefiting code quality and maintenance.
  • Developer Satisfaction and Happiness: Fosters a sense of confidence and fearlessness in dealing with the code, enabling developers to clean and optimize code effectively.
  • Team Satisfaction and Happiness: Enhances the morale and satisfaction of the supporting team, contributing to a more positive and productive work environment.

That's a pretty serious list, isn't it? But I have noticed that tests are very much neglected, especially in young companies and startups. This is due to several factors.
The first factor is time and money. Developers and stakeholders think that they don't have time to write tests. I can't agree with that. Test writing should become a part of any engineer's daily work. Early bug detection through testing is a significant advantage, as it helps identify and address issues early in the development cycle, saving both time and resources. Beyond technical benefits, software testing is cost-effective in the long run, preventing the high costs associated with fixing post-launch errors. A lot of benefits, and if you start right now, with your component, it will cost you almost nothing.
Just remember, race for high test coverage, while seemingly a noble pursuit, can be a flawed approach for many reasons, but one of them is maintenance overhead. Please, do not write tests just to increase coverage percentages, neglecting the actual purpose of testing, which is to ensure the application works correctly and is free from defects.
The second factor, developers don't know how and when to write tests. This may be lack of experience or lack of motivation. But at the end, tests increase developer satisfaction and happiness. Writing tests should be easy and fun. Testing is crucial for facilitating safe code refactoring and the addition of new features, ensuring that existing functionalities are not compromised.

Testing plays a vital role in building confidence and trust among users and stakeholders, promoting high-quality coding practices, and enhancing both developer and team satisfaction, leading to a more positive and productive work environment.

Unit testing

Unit tests are very low level and close to the source of an application. They consist in testing individual methods and functions of the classes, components, or modules used by your software. Unit tests are generally quite cheap to automate and can run very quickly by a continuous integration server.

When

The ideal use of unit testing is your components and functions. Test components for state changes and pure functions for its return value. A good example: design system components and utility functions.

Example

For unit testing in 2024 I would strongly recommend using Vitest and Testing Library. Today, both tools have already become a standard in Front-end testing. Despite the fact that Jest is still the most popular testing framework, it is inferior to Vitest in terms of execution speed and interesting features such as Hot Module Reload (HMR).

import { expect, test } from 'vitest'
import { faker } from '@faker-js/faker'
import { render, screen } from '@testing-library/react'
import NavigationBar from 'components/NavigationBar'

test('NavigationBar', () => {
    it('renders items correctly', async () => {
        let items = [  
            { slug: faker.lorem.slug(), url: faker.internet.url() },  
            { slug: faker.lorem.slug(), url: faker.internet.url() },  
        ]

        const { getByText } = render(<NavigationBar items={items} />)

        await fireEvent.click(getByText(items[0].slug))

        expect(getByRole("heading", { level: 1 })).toBe(items[0].slug)      
    })
})
Enter fullscreen mode Exit fullscreen mode

End-to-end testing

End-to-end testing replicates a user behavior with the software in a complete application environment. It verifies that various user flows work as expected and can be as simple as loading a web page or logging in or much more complex scenarios verifying email notifications, online payments, etc...

End-to-end tests are very useful, but they're expensive to perform and can be hard to maintain when they're automated. It is recommended to have a few key end-to-end tests and rely more on lower level types of testing (unit and integration tests) to be able to quickly identify breaking changes.

When

E2E testing involves testing user flows in an environment that simulates real user scenarios, like the browser. On the frontend, the ideal place for testing is the user's login and registration process, payment scenario.

Example

Until recently, using cypress and testing-library in E2E testing was a common practice. Even though these tools remain the most popular combination, I recommend using playwright for the next projects, and there are several reasons for this. The first reason is the incredible speed, compared to Cypress. If you've worked with Cypress before, remember how long it took you to launch a shell with a browser. Playwright does it about 3 times faster. The second reason is configuration. Cypress can take days to configure and debug. Playwright is amazingly easy to use and informative when debugging, and the command interface will configure your CI/CD pipeline as a gift.

import { test, expect } from '@playwright/test'  
import { faker } from '@faker-js/faker'

test.describe('Game Page Tests', () => {  
  test('should show a dialog window on a first page load', async ({ page }) => {  
    await page.goto(process.env.host)

    const dialogWindow = await page  
      .getByRole('dialog')  
      .locator('section')  
      .filter({ hasText: 'Hello world!' })  

    await expect(dialogWindow).toBeVisible()  

    const initials = faker.lorem.word()
    await page.getByPlaceholder('Enter initials').fill(initials)  
    await page.getByLabel('Save initials').click()  

    const heading = await page.locator('.main-header').getByText(initials)  
    await expect(heading).toBeVisible()  
  })})
Enter fullscreen mode Exit fullscreen mode

Accessibility testing

Accessibility testing is a process used to ensure that an application can be used by everyone, regardless of their ability or disability. This type of testing is important for making sure that all users can access and use the features and functionality of an application.

There are many different ways to test for accessibility, but some common methods include using screen readers or other assistive technologies, testing with users who have different disabilities, and checking for compliance with accessibility standards.

When

Accessibility testing remains a crucial aspect of software development to ensure applications and websites are usable by all people, including those with disabilities. Accessibility testing is critical in sectors where digital inclusivity is crucial, including government, education, commerce, finance, healthcare, corporate, and public services. It ensures compliance with legal standards and enhances the overall user experience for a wider audience. Any project or task that interfaces with a diverse user base, especially those that provide essential services or information, should prioritize accessibility testing. It not only helps in complying with legal requirements but also ensures a wider reach and a better user experience for all.

Example

Every year, the number of tools for this kind of testing grows larger and larger. And that's good! Developers and companies increasingly realize the importance of accessibility of services and applications. The choice of a tool depends on the project and its inclusiveness. I recommend always using eslint-plugin-plugin-jsx-a11y because it's just a static evaluation to identify accessibility issues, so there's no need to install and run tests. For more in-depth testing, we would need a high-level web browser automation API and toolkit. I recommend playwright, for its speed and ease of configuration, and axe-core, a powerful library for accessibility testing. It can be integrated into various testing frameworks and CI/CD pipelines. There is an @axe-core/playwright API for playwright that needs no configuration and is easy to use.

import { test, expect } from '@playwright/test'  

test.describe('homepage', () => {  
  test('should not have any automatically detectable accessibility issues', async ({  
    page,  
  }) => {  
    await page.goto(process.env.host)

    const accessibilityScanResults = await new AxeBuilder({ page }).analyze()

    expect(accessibilityScanResults.violations).toEqual([])
  })  
})
Enter fullscreen mode Exit fullscreen mode

Visual Regression Testing

Visual regression testing is a type of front-end testing that compares screenshots of the interface to see if they match the expected version. This test focuses on the interface of the application and crucial in front-end development for ensuring visual consistency, improving the quality of the user interface, saving time and resources in UI review processes, and fitting well into modern development practices and workflows. This is particularly useful in teams where developers and designers collaborate closely.

When

Anything that is visual and difficult to test in other ways: visual language, layout and company brand materials, icons, histograms, responsive Design Testing.

Example

For tests you can use any tool you like, like Playwright.Almost all e2e tools either include this feature or work in conjunction with them as dependencies. For 2024, I recommend setting up Storybook and Chromatic. This is the perfect tandem for documenting, organizing, and testing isolated components. Visual tester is automatic, you don't need to manually write tests. Just document components with Storybook and Chromatic automatically turns stories into tests. So the more coverage of your UI with stories, the more time you'll save your team with our automation. Chromatic serves as a single hub that allows you to test, approve PRs, and share feedback. During the pull request, Chromatic automatically runs in CI when you push code. There are two key workflows: UI Tests and UI Review.

Integration testing

Integration tests verify that different modules or services used by your application work well together. For example, it can be testing the interaction with the database or making sure that microservices work together as expected. These types of tests are more expensive to run as they require multiple parts of the application to be up and running.

When

In web applications, integration testing aims to detect issues in the interaction between various components, such as the front-end, back-end, database, and external APIs. This is crucial because even if individual components (units) work flawlessly on their own, they might fail when integrated. For instance, it might involve testing how the client-side interface interacts with the server-side application or how the application communicates with the database.

Example

If something goes wrong, you simply don't release until the bugs are fixed. If your front-end and back-end are developed separately, you may encounter difficulties here. Tests should be run independently so that they do not block each other. The error response rate and recovery rate should remain fast. In this case, if we imagine that back-end writes tests on its side and front-end on its side, the main task of integration tests is communication between interface and server API. In 2023, I tried Pact and I found its idea very interesting. Pact is a code-first tool for testing HTTP and message integrations using contract tests. Contract tests assert that inter-application messages conform to a shared understanding that is documented in a contract. Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests. Contract testing is a technique for testing an integration point by checking each application in isolation to ensure the messages it sends or receives conform to a shared understanding that is documented in a "contract".

How testing with Pact looks like

In general, "contract" testing is creating a pact between a consumer (for example, a client that wants to receive some data) and a provider (for example, an API on a server that provides the data the client needs).

Client (Consumer) test

const provider = new Pact({
    consumer: 'BookClient',
    provider: 'BookService',
    port: 3000,
    log: path.resolve(process.cwd(), 'logs', 'book_service_pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'),
    logLevel: 'INFO',
});

describe('Books Service', () => {
    describe('When a request to list all books is made', () => {
        beforeAll(() =>
            provider.setup().then(() => {
                provider.addInteraction({
                    uponReceiving: 'a request to list all books',
                    withRequest: {
                        method: 'GET',
                        path: '/books',
                    },
                    willRespondWith: {
                        status: 200,
                        body: eachLike({
                            id: 1,
                            title: like('Book Title 1'),
                            author: like('Author Name'),
                            year: like(2015),
                        }, {
                            min: 3
                        }),
                    },
                });
            })
        );
        test('should return the correct data', async () => {
            const response = await fetchBooks(URL, PORT);
            expect(response[0].title).toBe('Book Title 1');
            expect(response[0].author).toBe('Author Name');
            expect(response[0].year).toBe(2015);
        });
        afterEach(() => provider.verify());
        afterAll(() => provider.finalize());
    });
});
Enter fullscreen mode Exit fullscreen mode

Server (Provider) Test

describe('Pact Verification', () => {
    test('should validate the expectations of our consumer', () => {
        const opts = {
            provider: 'Provider',
            providerBaseUrl: 'http://localhost:3000',
            pactBrokerUrl: process.env.PACT_BROKER_URL,
            pactBrokerToken: process.env.PACT_BROKER_TOKEN,
            publishVerificationResult: true,
            providerVersion: '1.0.0',
            logLevel: 'INFO',
        };
        return new Verifier(opts).verifyProvider();
    });
});
Enter fullscreen mode Exit fullscreen mode

Sharing the contracts with the provider team

After successfully creating and executing your consumer tests, which generated a pact file as an output, the next step is to distribute this contract to the team in charge of the Order API. This is essential for them to verify that their API fulfills all the requirements outlined in the pact. While there are various methods to share pacts, it is advisable to use a Pact Broker. The Pact Broker is the preferred option because it facilitates efficient and automated workflows.

Testing tools you should try in 2024

Faker

Faker.js is a library for generate massive amounts of fake (but realistic) data for testing and development. Try not to use static data in your tests. Instead, use generated, close to real data. This will bring your tests closer to real-life scenarios, which means you will get realistic, close-to-user results.

MSW

MSW is is an API mocking library that allows you to write client-agnostic mocks and reuse them across any frameworks, tools, and environments. It allow to Intercept both REST and GraphQL API and use it as real API. No configurations, adapters, or plugins. Reuse the same mocks across environments and tools, be it an integration test with Vitest, an automated browser test with Playwright, a demo showcase in Storybook, or a React Native app. Or all of them at once.

UVU

UVU is an extremely fast and lightweight test runner for Node.js and the browser. I recommend it especially for Solid.js developers and node.js unit testing.

Happy DOM

Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML. I use it in my tests instead of Jest DOM. Can be used without configuration, just specify // @vitest-environment happy-dom on the top of test file.

Conclusion

Software testing is a complicated and big topic. Hopefully this guide will help you choose correct testing tools and understandings of that and how to test your front-end. I'm also hoping you will motivate and have a fun with tests.

If you are left confused or wanting more resources about a topic I touched on, don't hesitate to leave a comment below. Also, feel free to share your testing tools and ideas in comments.

Top comments (0)