react-native-testing-library: how to test useEffect with act
Asked Answered
S

8

22

I am using react-native-testing-library to test my react-native component. I have a component (for the purpose of this post, it has been over simplified):

export const ComponentUnderTest = () => {

 useEffect(() => {
   __make_api_call_here_then_update_state__
 }, [])

 return (
   <View>
     __content__goes__here
   </View>
 )
} 

Here is my (simplified) component.spec.tsx:

import { render, act } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';

test('it updates content on successful call', () => {
   let root;
   act(() => {
      root = render(<ComponentUnderTest />); // this fails with below error message
   });    
   expect(...);
})

Now when I run this code, I get this error: Can't access .root on unmounted test renderer

enter image description here

I don't even now what this error message means. I followed the docs from the react-native-testing-library on how to test with act and useEffect.

Any help would be greatly appreciated. Thanks

Schizogony answered 1/12, 2019 at 23:57 Comment(1)
Hi. I have the same problem.. Did you find a solution to this?Avicenna
H
17

I found a workaround:

import { render, waitFor } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';

test('it updates content on successful call', async () => {
   const root = await waitFor(() =>
       render(<ComponentUnderTest />);
   );   
   expect(...);
})
Heiduc answered 20/7, 2020 at 20:41 Comment(2)
This does not work, time goes on and nothing happens.Carleycarli
It worked in my case - thanks!Borrero
B
5

You can do it using: @testing-library/react-native

Example:

import { cleanup, fireEvent, render, debug, act} from '@testing-library/react-native'

afterEach(() => cleanup());

test('given correct credentials, gets response token.', async () => {
    const { debug, getByPlaceholderText, getByRole } = await render(<Component/>);

    await act( async () => {
            const emailInput = getByPlaceholderText('Email');;
            const passwordInput = getByPlaceholderText('Password');
            const submitBtn = getByRole('button', {name: '/submitBtn/i'});

            fireEvent.changeText(emailInput, 'email');
            fireEvent.changeText(passwordInput, 'password');
            fireEvent.press(submitBtn);
    });
});

Should work with useEffect also but I haven't tested it out myself. Works fine with useState.

Balch answered 26/2, 2021 at 13:9 Comment(6)
if you only need to wait for a useEffect to be triggered once after 1st render, what do you wrap act around?Vernacularism
I am wondering the answer of the same question @Vernacularism has askedAvicenna
@FatihTaşdemir @Vernacularism I think the warning to came up whilst I was running the tests. Find* queries are 'asynchronous' sometimes. So, I could await each query but I'm not sure which ones are async. So I wrapped them all with act() to handle it.Balch
If I wrap my tests in act() I get a console log error warning that I have overlapping act calls which is not supportedVernacularism
@Vernacularism You're using one act() or two? Other solutions call to use waitFor() when expect()ing the the result. Which essentially waits for an assertion to pass if components or user events take time to render or are async. Not sure about the error caused by act(). Some solutions work around act(). React testing library doesn't have a concrete documentation on act().Balch
@Balch I only wrote 1Vernacularism
P
3
root = render(<ComponentUnderTest />);

should be

 root = create(<ComponentUnderTest />);

----Full Code snippet. It works for me after above change

    import React, { useState, useEffect } from 'react'
    import { Text, View } from 'react-native'
    import { render, act } from 'react-native-testing-library'
    import { create } from 'react-test-renderer'
    
    export const ComponentUnderTest = () => {
      useEffect(() => {}, [])
    
      return (
        <View>
          <Text>Hello</Text>
        </View>
      )
    }
    
    test('it updates content on successful call', () => {
      let root
      act(() => {
        root = create(<ComponentUnderTest />) 
      })
    })

Platonic answered 3/12, 2019 at 16:6 Comment(6)
Thanks for the answer. But from which lib are you importing create? It seems that react-native-testing-library does not have such an exported memberSchizogony
react-test-renderer (which is already a dependency for react-native-testing-library)Platonic
I used create as you suggested. Unfortunately I am getting the same error. Some similar error/issue (on react, not react-native, with corresponding @testing-library/react) reported a problem with miss-match versions. (see github.com/testing-library/react-hooks-testing-library/issues/…) I don't know what would be the correct versions for meSchizogony
I've updated my answer with full code. It works when I use create. With render I get the same errorPlatonic
RNTL says: By default any render, update, fireEvent, and waitFor calls are wrapped by this function, so there is no need to wrap it manually. This method is re-exported from react-test-renderer.Selfpossession
Defeats the point no? We wanted render to use the queries: getByRole, getByTestID etc. Unless there is some other way to find elements to use fireEvents with, I can't see much use to create in this scenario. I can't find much docs or examples on create either.Balch
C
3

The following steps solved my case:

  • Upgrading React and react-test-renderer versions to 16.9 or above which support async functions inside act (both packages need to be the same version as far as i know)

  • Replacing react-native-testing-library's render with react-test-renderer's create as @helloworld suggested (Thank you kind sir, it helped me out)

  • Making the test function async, preceding the act with await and passing an async function to it

The final result looked something like this:


    test('it updates content on successful call', async () => {
      let root
      await act(async () => {
        root = create(<ComponentUnderTest />) 
      })
    })
Cliquish answered 26/4, 2020 at 15:58 Comment(3)
how did you combine with getTestById etc then since create doesn't have them and i believe it is shallow.Kellie
const { getByTestId } = await waitFor<RenderAPI>(() => { return render(component); });Reborn
Thank you, it helped me take the right snapshot after several useEffects and useStates were executed in the componentScurlock
S
0

The approach I'm using for testing asynchronous components with useEffect that triggers a rerender with setState is to set the test case up as normal, but use waitFor or findBy to block assertions until the component rerenders with the fetched data.

Here's a simple, runnable example:

import React, {useEffect, useState} from "react";
import {FlatList, Text} from "react-native";
import {render} from "@testing-library/react-native";

const Posts = () => {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    const url = "https://jsonplaceholder.typicode.com/posts";
    fetch(url).then(res => res.json()).then(setPosts);
  }, []);

  return !posts ? <Text>loading</Text> : <FlatList
    testID="posts"
    data={posts}
    renderItem={({item: {id, title}, index}) =>
      <Text testID="post" key={id}>{title}</Text>
    }
  />;
};

describe("Posts", () => {
  beforeEach(() => {
    global.fetch = jest.fn(url => Promise.resolve({
      ok: true,
      status: 200,
      json: () => Promise.resolve([
        {id: 1, title: "foo title"},
        {id: 2, title: "bar title"},
      ])
    }));
  });

  it("should fetch posts", async () => {
    const {findAllByTestId} = render(<Posts />);
    const posts = await findAllByTestId("post", {timeout: 500});
    expect(posts).toHaveLength(2);
    expect(posts[0]).toHaveTextContent("foo title");
    expect(posts[1]).toHaveTextContent("bar title");
    expect(fetch).toHaveBeenCalledTimes(1);
  });
});

This doesn't give me any act warnings, but I've had my share of those. This open GitHub issue appears to be the canonical resource.

Packages used:

{
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-native": "^0.64.0",
    "react-native-web": "^0.15.6"
  },
  "devDependencies": {
    "@babel/core": "^7.13.15",
    "@testing-library/jest-native": "^4.0.1",
    "@testing-library/react-native": "^7.2.0",
    "babel-jest": "^26.6.3",
    "jest": "^26.6.3",
    "metro-react-native-babel-preset": "^0.65.2",
    "react-test-renderer": "^17.0.2"
  }
}

And in the Jest config:

setupFilesAfterEnv: ["@testing-library/jest-native/extend-expect"],

for the .toHaveTextContent matcher. Or you can use an import:

import "@testing-library/jest-native/extend-expect";
Stubblefield answered 7/11, 2021 at 9:31 Comment(0)
T
0

Try it this way:


    it("should render <Component/>", async () => {
      await act(() => {
        render(<Activate />);
      });
    });
Tori answered 11/8, 2022 at 17:41 Comment(0)
A
0

You can use useEffects in your RNTL tests quite easily:

import { render, act } from '@testing-library/react-native';
import { ComponentUnderTest } from './componentundertest.tsx';

test('it updates content on successful call', () => {
   render(<ComponentUnderTest />)  
   expect(await screen.findByText('Results)).toBeTruthy(); // A
})

There is no need to use act directly, RNTL uses it for you under the hook.

The exact predicate to be used on line A depends on the component changes you do in your useEffect callback. Here I just assume that when fetching succeeds there is some Text component displaying "Results" text.

Important thing to note is that your fetching is probably async so you need to use findBy* queries which will wait for async action to happen (default timeout it ~5000 ms, it can be tweaked).

Another thing to note, it's a good practice to mock network calls so your tests do not call true API. There are various reason for that, test execution speed, test stability, not always being able to achieve desired API response for testing purposes, etc. Recommend tool would be MSW library.

Accroach answered 6/11, 2022 at 10:37 Comment(0)
E
0

You need to use waitFor to wait for asynchronous requests to complete.

Here's an updated code snippet with an explanation:

import { render, waitFor } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';

test('it updates content on successful call', async () => {
  // Mocking a successful API response
  yourMockApi.get.mockResolvedValue({});
    
  // Rendering the component under test
  render(<ComponentUnderTest />);
    
  // Wait for the API call to be made
  await waitFor(() => expect(yourMockApi.get).toBeCalled());
});

Explanation:

  • The yourMockApi.get method is being mocked to return a successful response using mockResolvedValue.
  • The waitFor function is being used to wait until the mocked API call is made before continuing with the test.
  • The await keyword is used to wait for the waitFor function to complete before continuing with the test.
Euonymus answered 27/2, 2023 at 21:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.