Search code examples
reactjsreact-hooksreact-router-domreact-testing-library

How to test a ProtectedRoute with RTL


I am banging my head over the wall since too long. I am not able to do a successful test for my ProtectedRoute component .

protected-route.js :

// src/components/protected-route/protected-route.js
import React from "react";
import { Outlet, Navigate } from "react-router-dom";
import { useAuth } from "../../utils/AuthContext.js";

const ProtectedRoute = () => {
  const { isAuthenticated, authStateFetched } = useAuth();

  if (!authStateFetched) {
    return null; // Render nothing until the authentication state has been fetched
  }

  if (!isAuthenticated) {
    return <Navigate to="/" replace={true} />;
  }

  return <Outlet />;
};

export default ProtectedRoute;

AuthContext.js

// src/utils/AuthContext.js
import { createContext, useContext, useState, useEffect } from "react";

const AuthContext = createContext();

export const useAuth = () => {
  return useContext(AuthContext);
};

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [authStateFetched, setAuthStateFetched] = useState(false);

  useEffect(() => {
    const fetchAuthState = async () => {
      try {
        const response = await fetch("http://localhost:3000/is-authenticated");
        const data = await response.json();
        setIsAuthenticated(data.isAuthenticated);
      } catch (error) {
        console.error("Error fetching authentication state:", error);
      } finally {
        setAuthStateFetched(true);
      }
    };

    fetchAuthState();
  }, []);

  const login = () => {
    setIsAuthenticated(true);
  };

  const logout = () => {
    setIsAuthenticated(false);
  };

  const value = {
    isAuthenticated,
    authStateFetched,
    login,
    logout, // Added the 'logout' function here
  };

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

protected-route.test.js

// src/components/protected-route/protected-route.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import { MemoryRouter, Routes, Route } from "react-router-dom";
import ProtectedRoute from "./protected-route";
import { AuthProvider, useAuth } from "../../utils/AuthContext.js";

jest.mock("../../utils/AuthContext.js");

const TestComponent = () => <div>Protected content</div>;

describe("ProtectedRoute", () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  test("renders protected content when authenticated", () => {
    useAuth.mockReturnValue({ isAuthenticated: true, authStateFetched: true });

    render(
      <AuthProvider>
        <MemoryRouter initialEntries={["/protected"]}>
          <Routes>
            <Route path="/protected" element={<ProtectedRoute />}>
              <Route index element={<TestComponent />} />
            </Route>
          </Routes>
        </MemoryRouter>
      </AuthProvider>
    );

    expect(screen.getByText("Protected content")).toBeInTheDocument();
  });
});

error message

 ProtectedRoute
    × renders protected content when authenticated (25 ms)

  ● ProtectedRoute › renders protected content when authenticated

    TestingLibraryElementError: Unable to find an element with the text: Protected content. This could be because the text is broken up by
multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    Ignored nodes: comments, script, style
    <body>
      <div />
    </body>

      30 |     );
      31 |
    > 32 |     expect(screen.getByText("Protected content")).toBeInTheDocument();
         |                   ^
      33 |   });
      34 | });
      35 |

Comments

As you can see, the TestComponent is not returned, I should have Protected content in the page.

Question

How to do a working test for my protected route

Thank you


Solution

  • Mocking the response of the HTTP request is better. See testing-recipes.html#data-fetching

    Instead of calling real APIs in all your tests, you can mock requests with dummy data. Mocking data fetching with “fake” data prevents flaky tests due to an unavailable backend, and makes them run faster.

    E.g.

    protected-route.test.jsx:

    import React from "react";
    import { render, screen, act } from "@testing-library/react";
    import '@testing-library/jest-dom';
    import { MemoryRouter, Routes, Route } from "react-router-dom";
    import ProtectedRoute from "./protected-route";
    import { AuthProvider } from "./AuthContext";
    
    const TestComponent = () => <div>Protected content</div>;
    
    describe("ProtectedRoute", () => {
      afterEach(() => {
        jest.clearAllMocks();
      });
    
      test("renders protected content when authenticated", async () => {
        global.fetch = jest.fn().mockResolvedValue({
          json: () => Promise.resolve({ isAuthenticated: true }),
        });
    
        await act(async () => {
          render(
            <AuthProvider>
              <MemoryRouter initialEntries={["/protected"]}>
                <Routes>
                  <Route path="/protected" element={<ProtectedRoute />}>
                    <Route index element={<TestComponent />} />
                  </Route>
                </Routes>
              </MemoryRouter>
            </AuthProvider>
          );
        });
    
        expect(screen.getByText("Protected content")).toBeInTheDocument();
      });
    });
    

    Test result:

     PASS  stackoverflow/76133719/protected-route.test.jsx
      ProtectedRoute
        ✓ renders protected content when authenticated (37 ms)
    
    ---------------------|---------|----------|---------|---------|-------------------
    File                 | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ---------------------|---------|----------|---------|---------|-------------------
    All files            |   92.18 |    77.77 |   66.66 |   92.18 |                   
     AuthContext.jsx     |   93.33 |       80 |      60 |   93.33 | 20,30,34          
     protected-route.jsx |   89.47 |       75 |     100 |   89.47 | 13-14             
    ---------------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        1.028 s
    

    package versions:

    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "jest": "^29.4.3",