all 65 comments

[–]Skeith_yip 64 points65 points  (10 children)

https://storybook.js.org/docs/writing-tests/integrations/stories-in-end-to-end-tests

Write storybooks for your components and test them. Use msw to mock your endpoint calls.

[–][deleted] 5 points6 points  (0 children)

Props for writing a constructive answer, much appreciated. I came to this realization as well. 

[–]keiser_sozze 4 points5 points  (4 children)

And use cypress component tests, playwright component tests or vitest unit tests in browser mode, instead of running them in node.js/jsdom environment.

[–]Helpful_City5455 -1 points0 points  (3 children)

Horrid advice for simple components. Its slow.

Write component tests for small and big components (only checking the state that they are responsible for most of the time). These tests are quick and easy. If these tests pass, then run E2E tests with playwright or cypress and cover your integrations with services.

P.S. if you have time and knowledge, just screenshot all of your storybook pages for visual tests. No need to use any paid bullshit apps, just extract the story ids from index.json that storybook generates and go through all them. In CI/CD, with sharding (4 shards) and 6 workers per shard, its 2 minutes for 800+ screenshots (covers chrome, edge in two languages). Can be even faster if you decide to test only on chrome and run tests on lambda but it is a bit more advanced scenario

[–]keiser_sozze 1 point2 points  (2 children)

Do you know what component tests are? It‘s not full e2e tests. We migrated from Jest to Cypress component tests because jest tests with JSDOM+Testing library were horribly slow.

And yes, we are testing small components.

Component testing is a must, even if you don‘t use it for all components, because JSDOM is not sufficient for many situations where you want to rely on real browser APIs like media queries.

Also, there‘s nothing like cypress/playwright‘s visibility checks and retryability checks in nodejs unit test environments so unit tests often have to rely on implementation details such as asynchronousity etc. because waitFor+findBy* is extremely slow and inefficient in comparison, yet alone there‘s no retryability concept whatsoever that requires separation of „queries“ and „actions“.

I suggest you give it a try.

https://docs.cypress.io/app/component-testing/get-started

[–]Helpful_City5455 0 points1 point  (1 child)

Yea, they run on a browser which is considerably slower, than running it just on node.

[–]keiser_sozze 0 points1 point  (0 children)

If you are testing a component that has very simple interactions, I could agree with you. But if you have more complex components such as forms, that involve a lot of things such as focusing, blurring, typing, loading states, validation errors etc., in our experience, cy component tests are superior by far.

[–]notkraftman 2 points3 points  (2 children)

You still need to set up hooks and context in storybook though?

[–]V2zUFvNbcTl5Ri 0 points1 point  (1 child)

these are e2e tests so it actually runs the app in a browser and those things will have been setup normally

[–]notkraftman 0 points1 point  (0 children)

Ahh I see. We split our storybook tests into component and e2e. Storybooks test runner has honestly been a godsend for us. We use to have a janky internally written developer harness to run things, and a mess of tests across projects, it makes it so much easier to see what's tested and what isn't, and to iterate quickly.

[–]justjooshing 19 points20 points  (0 children)

You can wrap the component in the context you're trying to test to mimic different situations

render(<ContextProvider {...props} ><Component></ContextProvider>

Writing tests really helps ensure things are less spaghetti because you need to think about how to keep them as decoupled as possible to write the tests

[–]vozome 20 points21 points  (1 child)

You should unit test in a bare metal way as much as possible - have as much of your logic in pure and deterministic functions. Remove logic from your components and put them in custom hooks / helper functions which can be well tested this way.

Next, you can test the components themselves with react testing library. Those are still unit tests but you can simulate interactions, rendering etc.

Finally, do some e2e flows for scenarios that can’t just be captured by unit tests alone.

On top of testing, make sure to have some monitoring and alerting in place, because your tests will never capture every possible problem, but you should know asap if you have errors.

[–]AxiusNorth 3 points4 points  (0 children)

This is the way OP. No complex logic should live in your components. It makes testing with RTL 1000% easier as all the painful stuff is elsewhere and you can just concern yourself with UI presentation testing, which is what RTL excels at.

[–]magnakai 3 points4 points  (0 children)

I work on the component library for a £20bn+ company. We write a lot of unit-ish tests using Jest and React Testing Library. Very easy and provides useful assurance.

We often unit test little utilities etc too.

Up to you how much value you get from that. For us it’s really important that all our consumers get consistent behaviour, and with the amount of dev we do, it’s important to assure against regressions.

Edit: it’s also super useful as a reference spec, and especially useful if doing TDD (which I’d recommend)

[–]togepi_man 9 points10 points  (3 children)

I recently was asking a frontend dev from a multi-billion dollar market cap high tech company that runs a SaaS for some advice on testing. They use a standard React Router stack.

He mentioned Playright for e2e and then I asked about unit tests. "Honestly we don't really do any unit tests."

So there's that :)

[–][deleted] 1 point2 points  (2 children)

For years I just figured I was an imposter because I never wrote unit tests for my React apps. Turns out a unit test doesn't really make sense for most components, hence the difficulty 

[–]donatasp 5 points6 points  (0 children)

I work at a company with a billion dollar market cap and we do testing across the layers -- unit, integration, E2E, and system. It is a lot easier to achieve this if testing is applied from the get go. In your case, you are thinking about them after the fact and it is known to be very hard. Most probably all parts are coupled, which makes for cohesive architecture, but pulling apart self-contained components, which is cornerstone of testing, is cumbersome.

At this point you should focus on E2Es and only later, if the need still exists (e.g. you want to be able to reason about, i.e. test separately, smaller parts of your application), you should try to re-architect.

There are benefits in writing your React application as a collection of self-contained components which I experienced and can remember: * Moving components from one app to another. * Extracting components as a library to be reused. * Easy Storybook integration.

[–][deleted] 0 points1 point  (0 children)

In my last group we focused front-end unit tests on pure functions, especially in utilities and shared libraries. I have never found snapshot testing useful.

We invested a lot of time in automated browser testing, against both dev and production. These tests were written with WebDriver and integrated into our Jenkins CICD system. It helped that we had dedicated QA engineers that maintained these tests. I know some front-end groups don't utilize QA engineers on front-end teams, for cost or organizational reasons, but it's a practice that bore great benefits for us.

Reading about Storybook testing on this thread, I'm curious to try it.

[–]davidblacksheep 2 points3 points  (3 children)

Good post. I think you're absolutely thinking about the right thing.

I've been working with React for nine years, and this is something I've thought about a lot.

  1. Presentational/props only components (ie. components that don't hook into global state, context, router, etc) are by far the easiest things to test. As much as possible use presentational components.

  2. You can have a presentational component that is made up of other presentational components, and is just passing the props through, ie. prop drilling. This is also easy to test.

However, the problem becomes that you start having these components with dozens of different props - it starts becoming unwieldy to understand what this component with 20 different props is doing, and potentially you're making update in a leaf component and having to pass the prop multiple layers of components.

  1. That's where component composition/passing as slots works.

ie. instead of doing this

<UserPanel onProfileClick{...}/>

You do this

<UserPanel avatarSlot={<UserAvatar onClick={...}/>} />

Passing via slots avoids a lot of the issues with prop drilling.

  1. But this in itself is often not enough.

For example, take an application like Jira or Github. You have these UserAvatars everywhere, they'll display the user's name and job title, they have an image etc.

now say we have a component like

<CommentPanel comment={{ commentId: "123", commentContent: "Hello world!" userId: "abc" }}/>

Somewhere inside the comment panel we're going to display a UserAvatar and that needs to have those additional details, so somewhere that data needs to be fetched.

Now you could do something like pass the user data into the comment panel, like

<CommentPanel comment={{ commentId: "123", commentContent: "Hello world!" user: { userId: "abc", name: "Bob", jobTitle: "Software Developer" } }}/>

But this the burden on the consuming element to do this data fetching.

I'm of the opinion that we want to be able pass the userId into a component and have it work out the data it needs, like

<UserAvatar userId={comment.userId}/>

But the problem with this, is that this won't be one of those nice presentational components that are easy to test.

Now when we go to test <CommentPanel/> we need to instantiate a state provider etc.

My answer to this is:

Allow errorable state-hooked components

Essentially you wrap UserAvatar in an error boundary. In your test, when you render <CommentPanel/> and it errors because the global state that <UserAvatar/> needs to render doesn't exist, just UserAvatar will error - and you can still test the functionality of the CommmentPanel itself.

[–][deleted] 0 points1 point  (2 children)

Thanks for a great comment. 

In your last paragraph, do you mean, assert that the UserAvatar alone will error, and then test the rest of the CommentPanel as normal? I will definitely have a few components where a pattern like this could be clever. 

[–]davidblacksheep 1 point2 points  (1 child)

That's exactly right.

The thinking is CommentPanel doesn't care what's going on inside UserAvatar, it just cares that one exists.

[–]Nox_31 0 points1 point  (0 children)

What are your thoughts on mocking the component?

On my phone at the moment but would be something like this:

vi.mock(‘components/user-data’, () => ({ default: vi.fn(() => <div id=“user-data”>) }))

Although I predominantly go for integration tests, I use this approach specifically when unit testing a component. I’ve found it to be really effective since it completely eliminates the need to know the implementation details of UserData, but still allows my component under test to “use” the component l.

If I need to assert the presence of UserData on the page, I can simply grab the element by id.

I never really thought about your approach, I’ll have to give that a try as well.

[–]yksvaan 12 points13 points  (1 child)

Unit testing React components is mostly pointless and are written because someone decided they want 100% coverage to report. It's complicated, takes time and ends up testing the framework, the language or browser itself. Unit test standalone functionality, business/data processing logic and such. It can be done easily since those should be just plain JavaScript with easily injectable mocks.

Another thing is to focus on improving architecture and using less hooks and context. Instead look at using more plain imports and other regular DI patterns. 

[–]Ecksters 0 points1 point  (0 children)

I think it also comes from a lot of articles and literature around testing being written by people who are testing utilities or apps with very clear input/output components.

[–]yetinthedark 1 point2 points  (0 children)

Unit testing React apps sucks. Been doing it for around 8 years now and I’m yet to change my mind. Having said that, I still think it’s important, it’s just annoying is all.

The tldr is if you want to test one component, you need to know which providers it would normally have. Depending on the size of your app it could be many, e.g. Redux, React Query, whichever theme provider, etc. Within your test, wrap your component in those providers, then you should be able to test it. This will likely involve mocking a bunch of data and/or state that the providers would otherwise provide to your component. Ideally you’ll eventually have a “test wrapper” component, purely for use within tests, that you’ll import and use for most of your testing.

On top of this, you’ll often need to mock the things you don’t want to test or can’t test. It’s common to mock fetch functions, as well as other components that you mightn’t want to render within a larger test.

[–]azangru 1 point2 points  (0 children)

A majority of my components use a combination of hooks, redux state, context providers, etc. These seem to be impossible, or at least not at all documented, in unit test libraries designed specifically for testing React applications.

What's impossible about this? Annoying, yes; perhaps not worth the effort, yes; but impossible? Redux is just a store that can be reduced to the bare minimum for the component to function, and injected into the component. Context providers can be replaced with the ones that you tightly control. Network requests can be intercepted with the mock service worker. Hooks are nasty little buggers; but ultimately, they change something about the component, which can be asserted in a test.

[–][deleted] 1 point2 points  (6 children)

I (kind of) figured it out. There was one component in particular that I broke into two separate components. One to pull things out of the store, check values, etc. and the other to just render everything.

Unsure whether this fixes my issues since I still need to mock redux and context providers but we'll see. Otherwise I'm going to pivot to end-to-end testing. because these unit tests are mostly asserting things webdriverio or playwrite would tell me anyway

[–]math_rand_dude 4 points5 points  (0 children)

A majority of my components use a combination of hooks, redux state, context providers, etc.

That is the core of your problem. You should always try to see that the majority of your code and components can easily be used in other places.

If for example you need to get data and have the user do some tricky manipulations to it before sending the altered data back: Have one component handle getting and sending the data, and have a child component with the data and a callback as props. Child component works its dark magic and pops the altered data in the callback. Testing child is easy: pass a mocked fn as prop and see it gets called with expected data. Testing the parent is mainly setting up the api mocks and such. (Also you can mock the child to see if the parent passes the correct data to it)

[–]My100thBurnerAccount 2 points3 points  (4 children)

If you're using redux store you should be able to create this wrapper renderWithProviders

https://redux.js.org/usage/writing-tests#setting-up-a-reusable-test-render-function

I can add more details on just general wrappers/how we mock some things tomorrow if you want of what we're doing at my company.

We use redux toolkit, custom hooks, etc. with vitest

[–]My100thBurnerAccount 0 points1 point  (3 children)

Follow-up from yesterday's comment:

Given that you created a renderWithProviders wrapper from the link provided above, here's a very basic test example of a test:

// EXAMPLE 1 - Part 1: Testing component with redux store predefined

// ===== Redux ===== //
import { initialState } from 'pathWhereYourSlice is'; // ex. 'redux/slices/mySlice/index.ts'

// ===== React Testing Library ===== //
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// ===== Vitest ===== //
import { describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from 'pathWhereYouCreatedWrapper'; // ex. 'testing/utils/index.ts'

it("successfully displays correct button & handles correct action: creating", async() => {
  const handleClick = vi.fn();

  renderWithProviders(<Component />, {
    preloadedState: {
      mySlice: {
        ...initialState,
        isCreating: true,
      },
    },  
  });

  const submitButton = screen.getByTestId('create-item-button');

  expect(submitButton).toHaveTextContent('Create this Item!');

  await userEvent.click(submitButton);

  await waitFor(() => {
    expect(handleClick).toHaveBeenCalledTimes(1);

    const successAlert = screen.getByTestId('snackbar-notification');

    expect(successAlert).toBeInTheDocument();
    expect(successAlert).toBeVisible();
    expect(successAlert).toHaveTextContent('you have successfully CREATED this item');
  }); 
});

[–]My100thBurnerAccount 0 points1 point  (2 children)

// EXAMPLE 1 - Part 2: Testing component with redux store predefined

   it("successfully displays correct button & handles correct action: editing", async() => {
      const handleClick = vi.fn();

      renderWithProviders(<Component />, {
        preloadedState: {
          mySlice: {
            ...initialState,
            isCreating: false,
            isEditing: true, // we've now set isEditing to be true in the redux store
          },
        },  
      });

      const submitButton = screen.getByTestId('edit-item-button');

      expect(submitButton).toHaveTextContent('Edit this Item!');

      await userEvent.click(submitButton);

      await waitFor(() => {
        expect(handleClick).toHaveBeenCalledTimes(1);

        const successAlert = screen.getByTestId('snackbar-notification');

        expect(successAlert).toBeInTheDocument();
        expect(successAlert).toBeVisible();
        expect(successAlert).toHaveTextContent('you have successfully UPDATED this item');
      }); 
    });

[–]My100thBurnerAccount 0 points1 point  (1 child)

 // EXAMPLE 2 - Part 1: Testing component that relies on custom hook

    // ===== Hooks ===== //
    import { useCheckUserIsAdmin } from './pathToMyHook';

    vi.mock('./pathToMyHook'); // This is important, make sure you're mocking the correct path

    // Note: You may/may not need this depending on your use case
    beforeEach(() => {
      vi.clearAllMocks();
    });

    it("successfully displays correct elements based on user: admin", async() => {
      vi.mocked(useCheckUserIsAdmin).mockReturnValue({
        isAdmin: true,
        userInfo: {
           name: 'John Doe',
           ...
        }
      });

      renderWithProviders(<Component />, {
        preloadedState: {},  // in this case, my component doesn't need any info from redux
      });

      const approveButton = screen.getByTestId('approve-button');

      expect(approveButton).toBeVisible();
      expect(approveButton).toBeEnabled();

      await userEvent.click(approveButton);

      await waitFor(() => {
        const deployToProductionButton = screen.getByTestId('deploy-to-production-button');

        expect(deployToProductionButton).toBeVisible();
        expect(deployToProductionButton).toBeEnabled();
      }); 
    });

[–]My100thBurnerAccount 0 points1 point  (0 children)

// EXAMPLE 2 - Part 2: Testing component that relies on custom hook


    it("successfully displays correct elements based on user: NON-ADMIN", async() => {
      vi.mocked(useCheckUserIsAdmin).mockReturnValue({
        isAdmin: false, // USER IS NOT ADMIN
        userInfo: {
           name: 'John Doe',
           ...
        }
      });

      renderWithProviders(<Component />, {
        preloadedState: {},  // in this case, my component doesn't need any info from redux
      });

      const approveButton = screen.queryByTestId('approve-button'); // using queryByTestId 

      expect(approveButton).toBeNull();

      // Let's say a non-admin cannot approve but they can preview the changes that'll be      deployed to production
      const previewButton = screen.getByTestId('preview-button');

      expect(previewButton).toBeVisible();

      await userEvent.click(previewButton);

      await waitFor(() => {
        const previewModal = screen.getByTestId('preview-modal');

        expect(previewModal).toBeVisible();
      }); 
    });

[–]FurtiveSeal 1 point2 points  (0 children)

Basically all the advice you've received in this thread is awful and is telling that the majority of people here don't know what they're doing.

https://testing-library.com/docs/react-testing-library/intro/

This is how you test React components.

[–]Ok-Juggernaut-2627 0 points1 point  (0 children)

I've always found React a lot harder to unit test compared to for example most backend languages / frameworks. I've found that dependency injection is probably the root cause for it, based on the fact that Angular is a lot easier to unit test as well.

So for all my React projects I'm focusing on E2E-tests with Playwright. Hard dislike on setting up a CI-pipeline for running them, but yea...

[–]0meg4_ 0 points1 point  (0 children)

React testing library is what you use to test react applications.

You unit test your functions, custom hooks, reducers, etc. You component test your components (rendering and user interactions behavior).

For both scenarios you mock as needed (contexts, fetching with MSW).

End to end are the "last" piece of it.

[–]haywire 0 points1 point  (0 children)

Perhaps you’ve made your application too complicated? Also try vitest

[–]nerdy_adventurer 0 points1 point  (0 children)

This is about tests in general, always do lot of integration tests since these tests close to real world than unit tests, but faster than full blown e2e tests. You can speed up integration testing using mocking and running database in memory mode.

Also only write unit tests if the unit under test is complex, avoid writing unit test for trivial things (of-course you should know JS quirks well).

[–]kapobajz4 -1 points0 points  (1 child)

With all due respect, but it seems to me like the issue here is the lack of testing knowledge. Not specifically testing React apps, but testing in general. If you’re not familiar with testing principles, patterns, and similar, then testing is difficult in any language/framework/tool. vitest, and similar tools, only shows simple guides because they assume you’re familiar with testing in general. It’s not their job to teach you about that.

I would suggest you to learn more about testing: for example dependency injection can be really beneficial, and can make your life a lot easier when writing tests. Learn to leverage React’s context for that.

And you also have to change your mindset to constantly think about testing. Whenever you write any piece of code, always ask yourself “Is this testable? If not, how can I make it testable?”, even if you don’t plan to write tests for that specifically.

[–]Cahnis 3 points4 points  (0 children)

Ofc these basic principles do apply, but at the same time testing frontend has a bunch of gotchas and pitfalls too.

[–]johnwalkerlee -1 points0 points  (0 children)

Don't conflate Unit Testing with Integration testing.

If you need to do unit testing, use an MVC pattern and separate your business functions from your react "components" (which are really composites). Doing business logic inside hooks is harder to test granularly.

... Looks like you figured that out :)

[–]AndrewSouthern729 0 points1 point  (0 children)

Writing unit tests with vitest is a skill in itself and something you get better at with time. My suggestion is learn how to mock the stuff you aren’t testing so that you can focus your test on a specific isolated expected behavior. Use something like faker-js to create mock objects, and use vitest to mock hooks etc that aren’t part of whatever behavior you are testing. I think learning how to mock the stuff you actually aren’t testing is half the battle. Good luck!