21

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

TheSoul
  • 4,906
  • 13
  • 44
  • 74

8 Answers8

16

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(...);
})
Lucas Mathioni
  • 169
  • 1
  • 3
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.

Tony
  • 436
  • 1
  • 9
  • 17
  • 1
    if you only need to wait for a `useEffect` to be triggered once after 1st render, what do you wrap `act` around? – conor909 Jul 23 '21 at 15:53
  • I am wondering the answer of the same question @conor909 has asked – Fatih Taşdemir Mar 14 '22 at 15:25
  • @FatihTaşdemir @conor909 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. – Tony Mar 15 '22 at 11:19
  • If I wrap my tests in `act()` I get a console log error warning that I have overlapping act calls which is not supported – conor909 Mar 15 '22 at 14:39
  • @conor909 You're using one `act()` or two? Other solutions call to use [`waitFor()`](https://testing-library.com/docs/dom-testing-library/api-async/#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](https://davidwcai.medium.com/react-testing-library-and-the-not-wrapped-in-act-errors-491a5629193b) solutions work around `act()`. React testing library doesn't have a concrete documentation on `act()`. – Tony Mar 15 '22 at 21:49
  • @Tony I only wrote 1 – conor909 Mar 22 '22 at 12:17
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 />) 
  })
})
helloworld
  • 2,179
  • 3
  • 24
  • 39
  • 1
    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 member – TheSoul Dec 03 '19 at 22:23
  • react-test-renderer (which is already a dependency for react-native-testing-library) – helloworld Dec 04 '19 at 14:11
  • 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 https://github.com/testing-library/react-hooks-testing-library/issues/151) I don't know what would be the correct versions for me – TheSoul Dec 04 '19 at 15:45
  • I've updated my answer with full code. It works when I use create. With render I get the same error – helloworld Dec 05 '19 at 06:58
  • 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. – Jose Sep 17 '20 at 10:17
  • 3
    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. – Tony Feb 22 '21 at 22:33
2

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 />) 
  })
})
FIIFE
  • 71
  • 1
  • 6
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";
ggorlen
  • 44,755
  • 7
  • 76
  • 106
0

try it this way

it("should render <Component/>", async () => {
  await act(() => {
    render(<Activate />);
  });
});
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.

Maciej Jastrzebski
  • 3,674
  • 3
  • 22
  • 28
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.