a.baulina
a.baulina

Reputation: 33

Mocking react custom hook return value as a module with Jest returns wrong value

I need to mock my custom hook when unit testing React component. I've read some stackoverflow answers but haven't succeeded in implementing it correctly.

I can't use useAuth without mocking it as it depends on server request and I'm only writing unit tests at the moment.

//useAuth.js - custom hook

import React, { createContext, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';

const authContext = createContext();

function useProvideAuth() {
    const [accessToken, setAccessToken] = useState('');
    const [isAuthenticated, setAuthenticated] = useState(
        accessToken ? true : false
    );

    useEffect(() => {
        refreshToken();
    }, []);

    const login = async (loginCredentials) => {
        const accessToken = await sendLoginRequest(loginCredentials);
        if (accessToken) {
            setAccessToken(accessToken);
            setAuthenticated(true);
        }
    };

    const logout = async () => {
        setAccessToken(null);
        setAuthenticated(false);
        await sendLogoutRequest();
    };

    const refreshToken = async () => {
        const accessToken = await sendRefreshRequest();
        if (accessToken) {
            setAccessToken(accessToken);
            setAuthenticated(true);
        } else setAuthenticated(false);
        setTimeout(async () => {
            refreshToken();
        }, 15 * 60000 - 1000);
    };

    return {
        isAuthenticated,
        accessToken,
        login,
        logout
    };
}

export function AuthProvider({ children }) {
    const auth = useProvideAuth();

    return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

AuthProvider.propTypes = {
    children: PropTypes.any
};

const useAuth = () => {
    return useContext(authContext);
};

export default useAuth;

The test I've written

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { NavBar } from '../App';

jest.resetAllMocks();
jest.mock('../auth/useAuth', () => {
    const originalModule = jest.requireActual('../auth/useAuth');
    return {
        __esModule: true,
        ...originalModule,
        default: () => ({
            accessToken: 'token',
            isAuthenticated: true,
            login: jest.fn,
            logout: jest.fn
        })
    };
});

describe('NavBar when isAuthenticated', () => {
    it('LogOut button is visible when isAuthenticated', () => {
        render(<NavBar />);
        expect(screen.getByText(/log out/i)).toBeVisible();
    });
});

The function I'm writing tests on:

//App.js
import React from 'react';
import cn from 'classnames';
import useAuth, { AuthProvider } from './auth/useAuth';
import './App.css';
import '../node_modules/bootstrap/dist/css/bootstrap.css';

function App() {
    return (
        <AuthProvider>
            <Router>
                    <NavBar />
            </Router>
        </AuthProvider>
    );
}

const NavBarSignUpButton = () => (
    <button className='button info'>
        Sign up
    </button>
);

const NavBarLogoutButton = () => {
    const auth = useAuth();
    const handleLogOut = () => {
        auth.logout();
    };

    return (
        <button className='button info' onClick={handleLogOut}>
            Log out
        </button>
    );
};

export const NavBar = () => {
    const isAuthenticated = useAuth().isAuthenticated;
    const loginButtonClassName = cn({
        btn: true,
        invisible: isAuthenticated
    });

    return (
        <nav className='navbar navbar-expand-lg navbar-light'>
            <div className='container'>
                <div className='d-flex justify-content-end'>
                    <div className='navbar-nav'>
                        <button className={loginButtonClassName}>
                            Log In
                        </button>
                        {isAuthenticated ? <NavBarLogoutButton /> : <NavBarSignUpButton />}
                    </div>
                </div>
            </div>
        </nav>
    );
};

The test code above doesn't throw any errors. However, the test fails as useAuth().isAuthenticated is always false (but I'm mocking it to return true). It doesn't change whether I test App or only NavBar

What am I doing wrong?

Upvotes: 1

Views: 5106

Answers (1)

Jakub Kotrs
Jakub Kotrs

Reputation: 6201

I made a super minified example that should show the mocking works. It just features the hook itself and a component returning YES or NO based on the hook. The test

useAuth.js

import {createContext, useContext} from 'react'

const authContext = createContext()

const useAuth = () => {
    return useContext(authContext)
}

export default useAuth

component.js

import useAuth from './useAuth'

export const Component = () => {
    const isAuthenticated = useAuth().isAuthenticated

    return isAuthenticated ? 'YES' : 'NO'
}

component.test.js

import React from 'react'
import {render, screen} from '@testing-library/react'
import {Component} from './component'

jest.mock('./useAuth', () => {
    const originalModule = jest.requireActual('./useAuth')
    return {
        __esModule: true,
        ...originalModule,
        default: () => ({
            accessToken: 'token',
            isAuthenticated: true,
            login: jest.fn,
            logout: jest.fn,
        }),
    }
})

describe('When isAuthenticated', () => {
    it('Component renders YES', () => {
        render(<Component />)
        screen.getByText(/YES/i)
    })
})

In this case, the component does in fact render YES and the test passes. This makes me thing there are other things involved. When I change the mock to false, the test fails because it renders NO.

Upvotes: 2

Related Questions