vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.88k stars 26.97k forks source link

Jest cannot test "use server" file #53065

Closed mkizka closed 1 year ago

mkizka commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: linux
      Arch: x64
      Version: #1 SMP Fri Jan 27 02:56:13 UTC 2023
    Binaries:
      Node: 18.16.0
      npm: 9.5.1
      Yarn: 1.22.19
      pnpm: 8.5.1
    Relevant Packages:
      next: 13.4.12-canary.0
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 4.9.5
    Next.js Config:
      output: N/A

warn  - Latest canary version not detected, detected: "13.4.12-canary.0", newest: "13.4.12".
        Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.

Which area(s) of Next.js are affected? (leave empty if unsure)

App Router, Jest (next/jest)

Link to the code that reproduces this issue or a replay of the bug

https://github.com/mkizka/nextjs-serveractions-jest

To Reproduce

git clone https://github.com/mkizka/nextjs-serveractions-jest
cd nextjs-serveractions-jest
yarn
yarn test

Describe the Bug

I got this message even though serverActions is true. This test passes if I remove "use server".

$ cat next.config.js
/** @type {import("next").NextConfig} */
module.exports = {
  reactStrictMode: true,
  experimental: {
    serverActions: true,
  },
};

$ yarn test
yarn run v1.22.19
$ jest
- warn You have enabled experimental feature (serverActions) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

 FAIL  app/action.test.ts
  ● Test suite failed to run

      × To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention
       ╭─[/home/ubuntu/nextjs-serveractions-jest/app/action.ts:1:1]
     1 │ "use server";
       · ────────────
     2 │
     3 │ export const action = () => {
     4 │   console.log("action called");
       ╰────

      3 | jest.spyOn(console, "log");
      4 |
    > 5 | test("action", () => {
        |                 ^
      6 |   action();
      7 |   expect(console.log).toHaveBeenCalledWith("action called");
      8 | });

      at Object.transformSync (node_modules/next/src/build/swc/index.ts:1000:25)
      at Object.<anonymous> (app/action.test.ts:5:17)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.15 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Expected Behavior

The test succeeds.

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1473

nicksan222 commented 1 year ago

In your config.jest.mjs add this line:

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: "node",   // Edit this line
};
mkizka commented 1 year ago

@nicksan222 Thank you! But I got the same result. https://github.com/mkizka/nextjs-serveractions-jest/pull/1

$ cat jest.config.mjs 
import nextJest from "next/jest.js";

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: "./",
});

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: "node",
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

$ yarn test
yarn run v1.22.19
$ jest
- warn You have enabled experimental feature (serverActions) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

 FAIL  app/action.test.ts
  ● Test suite failed to run

      × To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention
       ╭─[/home/ubuntu/workspace/nextjs-serveractions-jest/app/action.ts:1:1]
nicksan222 commented 1 year ago

@nicksan222 Thank you! But I got the same result. mkizka/nextjs-serveractions-jest#1

$ cat jest.config.mjs 
import nextJest from "next/jest.js";

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: "./",
});

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: "node",
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

$ yarn test
yarn run v1.22.19
$ jest
- warn You have enabled experimental feature (serverActions) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

 FAIL  app/action.test.ts
  ● Test suite failed to run

      × To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention
       ╭─[/home/ubuntu/workspace/nextjs-serveractions-jest/app/action.ts:1:1]

I think the issue is related to the fact that server actions are transformed into serverless functions. So it is quite difficult doing a unit testing for those

I suggest you to implement e2e testing tools like cypress or playwright for this job You could then make a separate function with the logic so as to test it with jest

ReangeloJ commented 1 year ago

I am experiencing the same issue too. Cant test my form component which calls a server action on submit

Amar97 commented 1 year ago

Same here

joyanedel commented 1 year ago

I got the same error, I tried different configurations on next.config.js but nothing seems to work I have two files

app/page.tsx import a server action from actions/submit.ts and uses it as formAction in a form component.

I'm not trying to test the action itself but only if some components (no action dependent) are loaded in the html

Kukunin commented 1 year ago

Is there a way to make Jest ignore "use server" directive at all?

I'm trying to write a unit test for my server actions (no React involved at all), and it cannot import the action if there is "use server" directive on the top of the file.

myFunction.ts:

"use server"

const myFunction = () => {}

export default myFunction

myFunction.spec.ts:

import myFunction from './myFunction'

it('works', async () => {
      await myFunction()
})

gives

TypeError: (0 , _myFunction.default) is not a function

The issue is different from the subject, but title is still relevant - I can't test that action because of "use server" directive.

Kukunin commented 1 year ago

as an alternative solution, it would be cool to mark the test file itself with 'use server', but then in my case Jest doesn't see any tests

Dinika commented 1 year ago

Something that works for me is to mock the module that uses server (i.e. that has the use server directive). For example, the component that I want to test (say ComponentA.tsx) renders a form. It imports a sever action from a file (say the path is @/server/actions.ts). This is the file that uses use server directive.

Here's how I test the ComponentA.tsx:

// ComponentA.spec.tsx

import { postQuestionToServer } from '@/server/actions'

jest.mock('@/server/actions.ts', () => ({
  __esModule: true,
  postQuestionToServer: jest.fn(), // The server action that is called when the form is submitted
}));

describe('ComponentA', () => {
     beforeEach(() => {
             (postQuestionToServer as jest.Mock).mockImplementation((data: FormData) => {
                 return Promise.resolve([]); // or whatever the success value should be
            });
     });
});

Since I don't really want to send a request to the server, this approach works well for me for unit testing the behavior of the client side component ComponentA.

Edit: I just realized that the above solution may not help if you want to test that the server action was called, however, you will be able to test other parts of the component.

test('does not work', () => {
    const spy = jest.spyOn(formActions, 'postQuestionToServer');

    const formEl = screen.getByTestId('my-form');
    fireEvent.submit(formEl);

    expect(spy).toHaveBeenCalled(); // This will NOT work
})
Kukunin commented 1 year ago

I encountered exactly the described issue after I updated my package.json packages, so next was updated to 13.4.19. After I downgraded it back to "next": "13.4.2",, all tests continued working.

So the issue must be somewhere between these versions.

When I run jest, I see the following input:

sergiy@sergiy-macbook project % yarn test
yarn run v1.22.19
$ jest --runInBand
- info Loaded env from /Users/sergiy/Work/project/.env.test.local
- info Loaded env from /Users/sergiy/Work/project/.env.test
- warn You have enabled experimental feature (serverActions) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

So you can see, that it respect serverActions in jest process in 13.4.2.

Hope this would help someone

unkhz commented 1 year ago

Mocking a server action defined in a "use server" file seems to work perfectly, as long as you specify the entire mock (automock fails). Granted it's not helpful for testing the impact of the action itself.

import { screen } from '@testing-library/react'
import { TodoList } from './TodoList';
import { addTodo } from './actions/addTodo';

jest.mock('./actions/addTodo', () => ({
  addTodo: jest.fn(),
}));

describe('TodoList', () => {
  it('calls action on submit', async () => {
    render(<TodoList />)
    fireEvent.input(screen.getByRole('textbox', { name: 'Message' }), { target: { value: 'new todo' }})
    fireEvent.click(screen.getByRole('button'))

    // wait for todos refreshed from server
    await screen.findByText('new todo')

    expect(addTodo).toHaveBeenCalledWith({ message: 'new todo'  })
  })
})
khRasikh commented 1 year ago

I am getting the same error while trying to run Jest Testing for Server Components: image

any idea?

craigmcc commented 1 year ago

I tried this again with the latest and greatest next.js release (13.5.2), and still get the same message:

`Test suite failed to run

  x To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#convention
   ,-[/Users/craigmcc/Git.Craigmcc/bookcase/src/actions/LibraryActions.ts:1:1]
 1 | "use server"
   : ^^^^^^^^^^^^
 2 | 

`

I do indeed have server actions enabled in my next.config.js file:

` /* @type {import('next').NextConfig} / const nextConfig = { experimental: { serverActions: true, }, }

module.exports = nextConfig `

huozhi commented 1 year ago

Hi can you upragde to v13.5.4-canary.6 , we land a change in that version to disable the SWC transform and erroring, so you should be able to test the components with basic UI testing tools

craigmcc commented 1 year ago

Mocking a server action defined in a "use server" file seems to work perfectly, as long as you specify the entire mock (automock fails). Granted it's not helpful for testing the impact of the action itself.

import { screen } from '@testing-library/react'
import { TodoList } from './TodoList';
import { addTodo } from './actions/addTodo';

jest.mock('./actions/addTodo', () => ({
  addTodo: jest.fn(),
}));

describe('TodoList', () => {
  it('calls action on submit', async () => {
    render(<TodoList />)
    fireEvent.input(screen.getByRole('textbox', { name: 'Message' }), { target: { value: 'new todo' }})
    fireEvent.click(screen.getByRole('button'))

    // wait for todos refreshed from server
    await screen.findByText('new todo')

    expect(addTodo).toHaveBeenCalledWith({ message: 'new todo'  })
  })
})

Mocking the server actions doesn't help my use case at all ... I am trying to test the actual logic of my server actions, and with the "use server" declaration present, I cannot do so. Without it, those tests (primarily fetch and mutation access to my database) work properly.

Making life even more complicated, though, now I cannot call those server actions in a client side component (which is kind of the point of these things). My current workaround is to create a "shim" module, with "use server" at the top, that just forwards all the calls to the action module that doesn't have this declaration. That is tedious and error prone, but at least I don't have to build and test an API layer to do this (i.e. the kind of thing I have to do with an express-based back end and a front end based on Create React App).

mkizka commented 1 year ago

The test is now successful. Thank you!

craigmcc commented 1 year ago

Yay! v13.5.4-canary.6 fixes it for me as well.

github-actions[bot] commented 1 year ago

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.