Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Hooks - How to test changes on global providers

I'm trying to test the following scenario:

  • A user with an expired token tries to access a resource he is not authorized
  • The resources returns a 401 error
  • The application updates a global state "isExpiredSession" to true

For this, I have 2 providers:

  • The authentication provider, with the global authentication state
  • The one responsible to fetch the resource

There are custom hooks for both, exposing shared logic of these components, i.e: fetchResource/expireSesssion

When the resource fetched returns a 401 status, it sets the isExpiredSession value in the authentication provider, through the sharing of a setState method.

AuthenticationContext.js import React, { createContext, useState } from 'react';

const AuthenticationContext = createContext([{}, () => {}]);

const initialState = {
  userInfo: null,
  errorMessage: null,
  isExpiredSession: false,
};

const AuthenticationProvider = ({ authStateTest, children }) => {
  const [authState, setAuthState] = useState(initialState);

  return (
    <AuthenticationContext.Provider value={[authStateTest || authState, setAuthState]}>
      { children }
    </AuthenticationContext.Provider>);
};


export { AuthenticationContext, AuthenticationProvider, initialState };

useAuthentication.js

import { AuthenticationContext, initialState } from './AuthenticationContext';


const useAuthentication = () => {
  const [authState, setAuthState] = useContext(AuthenticationContext);
  ...
  const expireSession = () => {
    setAuthState({
      ...authState,
      isExpiredSession: true,
    });
  };
  ...
  return { expireSession };
 }

ResourceContext.js is similar to the authentication, exposing a Provider

And the useResource.js has something like this:

const useResource = () => {
  const [resourceState, setResourceState] = useContext(ResourceContext);
  const [authState, setAuthState] = useContext(AuthenticationContext);

  const { expireSession } = useAuthentication();

  const getResource = () => {
    const { values } = resourceState;
    const { userInfo } = authState;

    return MyService.fetchResource(userInfo.token)
      .then((result) => {
        if (result.ok) {
          result.json()
            .then((json) => {
              setResourceState({
                ...resourceState,
                values: json,
              });
            })
            .catch((error) => {
              setErrorMessage(`Error decoding response: ${error.message}`);
            });
        } else {
          const errorMessage = result.status === 401 ?
            'Your session is expired, please login again' :
            'Error retrieving earnings';
          setErrorMessage(errorMessage);
          expireSession();

        }
      })
      .catch((error) => {
        setErrorMessage(error.message);
      });
  };
  ...

Then, on my tests, using react-hooks-testing-library I do the following:

  it.only('Should fail to get resource with invalid session', async () => {
    const wrapper = ({ children }) => (
      <AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
        <ResourceProvider>{children}</ResourceProvider>
      </AuthenticationProvider>
    );
    const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });

    fetch.mockResponse(JSON.stringify({}), { status: 401 });

    act(() => result.current.getResource());
    await waitForNextUpdate();

    expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
    // Here is the issue, how to test the global value of the Authentication context? the line below, of course, doesn't work
    expect(result.current.isExpiredSession).toBeTruthy();
  });

I have tried a few solutions:

  • Rendering the useAuthentication on the tests as well, however, the changes made by the Resource doesn't seem to reflect on it.
  • Exposing the isExpiredSession variable through the Resource hook, i.e:
      return { 
            ...
            isExpiredSession: authState.isExpiredSession,
            ...
       };

I was expecting that by then this line would work:

expect(result.current.isExpiredSession).toBeTruthy();

But still not working and the value is still false

Any idea how can I implement a solution for this problem?

like image 574
dfranca Avatar asked Jun 03 '19 12:06

dfranca


1 Answers

Author of react-hooks-testing-library here.

It's a bit hard without being able to run the code, but I think your issue might be the multiple state updates not batching correctly as they are not wrapped in an act call. The ability to act on async calls is in an alpha release of react (v16.9.0-alpha.0) and we have an issue tracking it as well.

So there may be 2 ways to solve it:

  1. Update to the alpha version and a move the waitForNextUpdate into the act callback
npm install [email protected]
  it.only('Should fail to get resource with invalid session', async () => {
    const wrapper = ({ children }) => (
      <AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
        <ResourceProvider>{children}</ResourceProvider>
      </AuthenticationProvider>
    );
    const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });

    fetch.mockResponse(JSON.stringify({}), { status: 401 });

    await act(async () => {
      result.current.getResource();
      await waitForNextUpdate();
    });

    expect(result.current.errorMessage).toEqual('Your session is expired, please login again');

    expect(result.current.isExpiredSession).toBeTruthy();
  });
  1. Add in a second waitForNextUpdate call
  it.only('Should fail to get resource with invalid session', async () => {
    const wrapper = ({ children }) => (
      <AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
        <ResourceProvider>{children}</ResourceProvider>
      </AuthenticationProvider>
    );
    const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });

    fetch.mockResponse(JSON.stringify({}), { status: 401 });

    act(() => result.current.getResource());

    // await setErrorMessage to happen
    await waitForNextUpdate();

    // await setAuthState to happen
    await waitForNextUpdate();

    expect(result.current.errorMessage).toEqual('Your session is expired, please login again');

    expect(result.current.isExpiredSession).toBeTruthy();
  });

Your appetite for using alpha versions will likely dictate which option you go for, but, option 1 is the more "future proof". Option 2 may stop working one day once the alpha version hits a stable release.

like image 83
Michael Peyper Avatar answered Oct 12 '22 02:10

Michael Peyper