Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to mock useSession? #775

Closed
3 of 5 tasks
laurivaananen opened this issue Oct 14, 2020 · 16 comments
Closed
3 of 5 tasks

How to mock useSession? #775

laurivaananen opened this issue Oct 14, 2020 · 16 comments
Labels
help needed The maintainer needs help due to time constraint/missing knowledge question Ask how to do something or how something works

Comments

@laurivaananen
Copy link

laurivaananen commented Oct 14, 2020

Your question
What's is the recommended way to mock useSession for unit tests with Jest and react testing library?

What are you trying to do

I have a component like this

import React from "react";
import { useSession } from "next-auth/client";

const Omega = () => {
  const [session] = useSession();
  return <div>Hello, {session?.user.name}</div>;
};

export default Omega;

and I wrote a test for it like this:

import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import Omega from "../../../components/Omega";
import { Provider } from "next-auth/client";
import "@testing-library/jest-dom";

describe("Omega", () => {
  it("Works", async () => {
    render(
      <Provider
        session={{
          expires: "1",
          user: { email: "a", name: "Delta", image: "c" },
        }}
      >
        <Omega />
      </Provider>
    );

    await waitFor(() =>
      expect(screen.queryByText("Hello, Delta")).toBeInTheDocument()
    );
  });
});

The test passes correctly except I get this error message in the console:

    console.error
      [next-auth][error][client_fetch_error] [
        '/api/auth/session',
        ReferenceError: fetch is not defined

It looks like next-auth is trying to fetch the session through api even though I provided it in the provider. What's the recommended way to mock sessions in unit tests?

Feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful
@laurivaananen laurivaananen added the question Ask how to do something or how something works label Oct 14, 2020
@laurivaananen laurivaananen changed the title How to mock useSessions? How to mock useSession? Oct 14, 2020
@iaincollins iaincollins added the help needed The maintainer needs help due to time constraint/missing knowledge label Oct 20, 2020
@iaincollins
Copy link
Member

Great question!

I don't have an immediate answer for you (maybe somebody else will) but there are at least a couple of ways of mocking fetch in Jest and using that approach mock responses from /api/auth/session is probably an easy way to emulate the behaviour of being signed in (or not).

@laurivaananen
Copy link
Author

Ok I managed to find a better way to mock it. Looks like I was overthinking it.

I changed my test to this to get it working nicely:

import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import Omega from "./Omega";
import client, { Session } from "next-auth/client";
import "@testing-library/jest-dom";
jest.mock("next-auth/client");

describe("Omega", () => {
  it("Works", async () => {
    const mockSession: Session = {
      expires: "1",
      user: { email: "a", name: "Delta", image: "c" },
    };

    (client.useSession as jest.Mock).mockReturnValueOnce([mockSession, false]);

    render(<Omega />);

    expect(screen.getByText("Hello, Delta")).toBeInTheDocument();
  });
});

@jhackett1
Copy link
Contributor

jhackett1 commented Mar 27, 2021

i did something similar. it would be great to get this added to the docs - perhaps near the cypress testing tutorial section?

import Layout from "./Layout"
import { render, screen } from "@testing-library/react"
import { useSession } from "next-auth/client"

jest.mock("next-auth/client")

describe("Layout", () => {
  it("renders correctly when signed out", () => {
    useSession.mockReturnValueOnce([false, false])

    render(<Layout />)
    expect(screen.getByText("Log in"))
  })

  it("renders correctly when signed in", () => {
    useSession.mockReturnValueOnce([
      {
        user: {
          email: "foo@bar.com",
        },
      },
      false,
    ])

    render(<Layout />)
    expect(screen.getByText("You are logged in as foo@bar.com"))
    expect(screen.getByText("Log out"))
  })
})

@balazsorban44
Copy link
Member

I can also suggest https://mswjs.io/

https://kentcdodds.com/blog/stop-mocking-fetch

@urgent
Copy link

urgent commented Apr 25, 2021

Jest snapshot throws this error because window does not exist. To fix, I factored out a view, and snapshot that. useSession is passed as props:

export default function Nav() {
  const [session, loading] = useSession();

  return <View session={session} loading={loading} />
}

@danilockthar
Copy link

Ok I managed to find a better way to mock it. Looks like I was overthinking it.

I changed my test to this to get it working nicely:

import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import Omega from "./Omega";
import client, { Session } from "next-auth/client";
import "@testing-library/jest-dom";
jest.mock("next-auth/client");

describe("Omega", () => {
  it("Works", async () => {
    const mockSession: Session = {
      expires: "1",
      user: { email: "a", name: "Delta", image: "c" },
    };

    (client.useSession as jest.Mock).mockReturnValueOnce([mockSession, false]);

    render(<Omega />);

    expect(screen.getByText("Hello, Delta")).toBeInTheDocument();
  });
});

Im trying bot the answers from the initial fetch error and nothing seems to work for me in the test. Im receiving this:

  TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

      21 |
      22 | const Header = () => {
    > 23 |   const [session, loading] = useSession();
         |                              ^
      24 |   const [isLoading, setIsLoading] = useState(false);
     

And this is how i implemented it : Any help?

import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import client, { session } from "next-auth/client";
import configureStore from "redux-mock-store";
import { initialState as infoState } from "../lib/reducers/information-block";
import { initialState as expState } from "../lib/reducers/experience-block";
im
```port { initialState as educState } from "../lib/reducers/education-block";
import { initialState as skillsState } from "../lib/reducers/skills-block";
import "@testing-library/jest-dom";
import Dashboard from "../pages/dashboard";
jest.mock("next-auth/client");

describe("Omega", () => {
  it("Works", async () => {
    const middlewares = [];
    const mockStore = configureStore(middlewares);
    // Initialize mockstore with empty state
    const initialState = {
      basics: {
        fullPreview: false,
      },
      info: infoState,
      experience: expState,
      education: educState,
      skills: skillsState,
    };
    const store = mockStore(initialState);
    const mockSession = {
      expires: "1",
      user: { email: "a", name: "Delta", image: "c" },
    };

    (client.useSession as jest.Mock).mockReturnValueOnce([mockSession, false]);

    render(
      <Provider store={store}>
        <Dashboard />
      </Provider>
    );

    expect(screen.getByText("Hello, Delta")).toBeInTheDocument();
  });
});

@ajaykarthikr
Copy link

@danilockthar Were you able to fix this issue? I am too facing this same issue.

@danilockthar
Copy link

danilockthar commented Jun 7, 2021

@ajaykarthikr not really. I tried using msw and jest-fetch-mock with half of success, the error "fetch is not defined " dissappear but i have another that says

"https://next-auth.js.org/errors#client_fetch_error session TypeError: Cannot read property 'json' of undefined"

@ajaykarthikr
Copy link

@danilockthar I was able to mock useSession using msw. I took inspiration from this PR #1992. You can try the same.

@danilockthar
Copy link

@ajaykarthikr can you share how you implemented it ?

@ajaykarthikr
Copy link

First I setup mock server to intercept rest api requests the NextAuth APIs.

mock.js

import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"

export const mockSession = {
  user: {
    image: null,
    name: "John",
    email: "john@email.com",
  },
  expires: 123213139,
}

export const mockProviders = {
  credentials: {
    id: "credentials",
    name: "Credentials",
    type: "credentials",
    authorize: null,
    credentials: null,
  },
}

export const mockCSRFToken = {
  csrfToken: randomBytes(32).toString("hex"),
}

export const mockCredentialsResponse = {
  ok: true,
  status: 200,
  url: "https://path/to/credentials/url",
}

export const mockSignOutResponse = {
  ok: true,
  status: 200,
  url: "https://path/to/signout/url",
}

export const server = setupServer(
  rest.post("/api/auth/signout", (req, res, ctx) =>
    res(ctx.status(200), ctx.json(mockSignOutResponse))
  ),
  rest.get("/api/auth/session", (req, res, ctx) =>
    res(ctx.status(200), ctx.json(mockSession))
  ),
  rest.get("/api/auth/csrf", (req, res, ctx) =>
    res(ctx.status(200), ctx.json(mockCSRFToken))
  ),
  rest.get("/api/auth/providers", (req, res, ctx) =>
    res(ctx.status(200), ctx.json(mockProviders))
  ),
  rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
    res(ctx.status(200), ctx.json(mockCredentialsResponse))
  ),
  rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
)

Then in the Login Page I check if the page has been reloaded after login.
login.js

import React from "react";
import { useRouter } from "next/router";

// Using render and screen from test-utils.js instead of
// @testing-library/react
import { server } from "../mocks";
import { render, screen, fireEvent, waitFor } from "../utils";
import Login from "@pages/index";

const { location } = window

beforeAll(() => {
  server.listen();
  delete window.location
  window.location = {
    ...location,
    replace: jest.fn(),
    reload: jest.fn(),
  }
});

beforeEach(() => {
  jest.resetAllMocks();
  server.resetHandlers();
});

afterAll(() => {
  window.location = location
  server.close()
})

jest.mock("next/router", () => ({
  useRouter: jest.fn(),
}))

describe("Login", () => {

  const push = jest.fn();

  useRouter.mockImplementation(() => ({
    push,
    pathname: "/",
    route: "/",
    asPath: "/",
    query: "",
  }));

  test("Test user login", async () => {

    const mockRouter = {
      push: jest.fn() // the component uses `router.push` only
    };
    (useRouter).mockReturnValue(mockRouter);

    const query = {};
    render(<Login query={query} />);

    // screen.debug();

    const heading = screen.getByText(
      /Welcome/
    );
    expect(heading).toBeInTheDocument();

    fireEvent.change(screen.getByPlaceholderText(/Email Address/), {
      target: { value: 'testuser@email.com' },
    });

    fireEvent.change(screen.getByPlaceholderText(/Password/), {
      target: { value: 'password' },
    });

    fireEvent.click(screen.getByText(/Log in/));

    await waitFor(() => {
      expect(window.location.replace).toHaveBeenCalledTimes(1);
    });
  });
});

Everthing works fine, the session object from useSession gets populated with data. But I am unable to check if the page has been reloaded. That's an issue I have to fix.

For other pages that require authentication you just need to start the server before each jest unit test.

@cg-n
Copy link

cg-n commented Jun 21, 2021

I was receiving the same TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator)) error until I changed this:

useSession.mockReturnValueOnce([false, false]);

to this:

useSession.mockReturnValue([false, false]);

@CrutchTheClutch
Copy link
Contributor

@cg-n Thanks! Your suggestion fixed my issue when I ran into this problem

@agangdi
Copy link

agangdi commented Mar 15, 2022

I'm using v4, and my mock code like this, but always get undefined with useSession, does anyone mock useSession with v4 successful ?

import { render, screen } from '@testing-library/react'
import Header from '../../components/layout/Header'
import "@testing-library/jest-dom";
import {useSession} from "next-auth/react";
jest.mock("next-auth/react");


describe("Header Component", () => {

  it('Show Log Out when has session',
    async () => {
      const mockSession = {
        expires: new Date(Date.now() + 2 * 86400).toISOString(),
        user: { username: "admin" }
      };
      (useSession as jest.Mock).mockReturnValueOnce([mockSession, 'authenticated']);
      // @ts-ignore
      // useSession.mockReturnValue([mockSession, 'authenticated']);
      render(<Header/>);
      expect(screen.getByText("LOG OUT")).toBeInTheDocument();
    })
})

@TomFreudenberg
Copy link
Contributor

Hi,

if interested, I wrote a package for mocking next-auth like:

import { render, screen } from '@testing-library/react'
import { withMockAuth } from '@tomfreudenberg/next-auth-mock/jest';
import SignoutPage from '@/pages/auth/signoutx';

describe('Pages', () => {
  describe('Signout', () => {
    it('should render want to sign out', () => {
      render(withMockAuth(<SignoutPage />, 'userAuthed'));
      expect(screen.getByText('Do you want to sign out?'));
    });
  });
});

Find the whole story at: https://github.com/TomFreudenberg/next-auth-mock

@brandonlenz
Copy link

brandonlenz commented Oct 18, 2022

@agangdi I was able to do something like this:

import { render, screen } from '@testing-library/react'
import MyPage from 'pages'
import userEvent from '@testing-library/user-event'

jest.mock('next-auth/react')
import { useSession, signIn, signOut } from 'next-auth/react'

const mockUseSession = useSession as jest.Mock
;(signIn as jest.Mock).mockImplementation(() => jest.fn())
;(signOut as jest.Mock).mockImplementation(() => jest.fn())

describe('My page', () => {

  const renderMyPage = () => {
    render(<MyPage />)

    const signInButton = screen.queryByRole('button', {
      name: 'Sign in with Provider',
    })
    const signOutButton = screen.queryByRole('button', {
      name: 'Sign out',
    })

    return {
      signInButton,
      signOutButton,
    }
  }

  it('renders when logged out', () => {
    const user = userEvent.setup()

    mockUseSession.mockReturnValue({
      status: 'unauthenticated',
      data: null,
    })

    const { signInButton, signOutButton } = renderMyPage()

    expect(signInButton).toBeInTheDocument()
    expect(signInButton).toHaveClass('usa-button')
    expect(signOutButton).not.toBeInTheDocument()

    await user.click(signInButton as HTMLElement)

    expect(signIn).toHaveBeenCalledTimes(1)
    expect(signIn).toHaveBeenCalledWith('cognito') // We're using cognito as the auth provider
  })
}

You can then also make use of UserEvent to verify that clicking on buttons calls the signIn/signOut mocked imports as well. Hope it helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help needed The maintainer needs help due to time constraint/missing knowledge question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests