test-utils icon indicating copy to clipboard operation
test-utils copied to clipboard

Support ESM

Open GMartigny opened this issue 4 years ago • 8 comments

Hi, More and more tools are using ESM modules and the support is increasing in the community.

Jest support ESM when "type": "module" is present in the package.json. On Nuxt documentation, every examples for the nuxt.config.js file are in ESM syntaxe.

But when setting up Nuxt with test-utils in ESM, an error is thrown Must use import to load ES Module: /home/node/app/nuxt.config.js because a require is used.

jest.config.js

export default {
  preset: '@nuxt/test-utils',
  transform: {},
};

nuxt.config.js

export default {
  srcDir: 'src/',
};

package.json

{
  "name": "terraforming",
  "type": "module",
  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "nuxt": "^2.15.7",
  },
  "devDependencies": {
    "@nuxt/test-utils": "^0.2.2",
    "jest": "^27.1.0",
  }
}
$ NODE_OPTIONS=--experimental-vm-modules npm test

GMartigny avatar Sep 10 '21 10:09 GMartigny

Do you have a repo or test setup to share? Usually, you can avoid the experimental options with some tweaks.

regenrek avatar Sep 21 '21 10:09 regenrek

@GMartigny Seems like the issue is related with Nuxt’s loadNuxtConfig helper which is used by the test-utils.

Possible workaround:

  • import your Nuxt config to a test file,
  • pass it to setupTest,
  • and add fixture: false, which will make the utils library avoid calling loadNuxtConfig
import { get, setupTest } from '@nuxt/test-utils'
import config from '../nuxt.config'

describe('ssr', () => {
  setupTest({
    config,
    fixture: false,
    server: true
  })

  it('renders the index page', async () => {
    const { body } = await get('/')

    expect(body).toContain('Hello world!')
  })
})

Unfortunately there are trade offs. The test-utils are creating random build directory for each build to avoid race conditions. With fixture: false this precaution will be disabled.

By the way, test-utils preset is unnecessary with your setup. The preset only adds testEnvironment: node (the new default since Jest v27) and includes transform option (you are overriding anyway).

mrazauskas avatar Sep 21 '21 11:09 mrazauskas

Here's a minimal repro repo.

It works until I added @nuxtjs/style-resources. Which is exporting ESM anyway, should I make it pass through babel ?

GMartigny avatar Sep 21 '21 12:09 GMartigny

can you look at this comment https://github.com/nuxt/test-utils/issues/165#issuecomment-923792271

regenrek avatar Sep 21 '21 13:09 regenrek

@GMartigny Are you planning to test your web app in a browser using Playwright? In this case it is better idea to use Playwright Test Runner instead of Jest (with Test Utils). See my comments here.

There are many reasons why Playwright Test Runner is a better choice. For example, your app will grow, you will want to split tests into several files. In case you use the Test utils, you will have to include setupTest in each file. Every time calling setupTest will build fresh app. That’s slow. Your app did not change, why to rebuild? To avoid that you can run nuxt build before running tests and disable built step:

setupTest({
    fixture: false,
    server: true,
    build: false,
    config: {
        buildDir: '.nuxt',
        ...config
    }
})

Using Playwright Test Runner you don’t need to repeat this boilerplate code. In general, Jest is great tool for unit testing, but for testing in browser Playwright Test Runner is way better choice. Check Playwright’s website, they recommend using their runner instead of any other solution. Simply try to set up tests so that: one particular test is running in only one particular browser; or running all tests in all browsers; or mocking some server response; not to mention the situation which I described above.

This will be complicated with Jest. It does not scale.

Also keep in mind that from very beginning Nuxt Test Utils were made to help testing Nuxt modules. Pass different settings to setupTest, build a fixture and test your module’s logic in various situations. You simply don’t need all this functionality to test web app.

mrazauskas avatar Sep 22 '21 08:09 mrazauskas

Back to your question. @nuxtjs/style-resources is not ESM module. File extension is .js, "type": "module" is not declared in package.json, so Node will treat this module as CJS, but will choke on export default (line 6). We can change file extension or add "type": "module", but require does not exist in ESM (it is used in the very first line). This code is written is Javascript, but according to Node’s specification this is not CJS, nor ESM module.

Should everything published on npm be a Node module? That’s not necessary at all. @nuxtjs/style-resources is a Nuxt module. Nuxt can load it, but Node cannot.

What about Jest? Jest follows Node’s module specification. So Jest cannot load Nuxt modules. Wait.. Nuxt CLI runs on Node and modules are loaded as expected. Why Jest cannot do the same? Well.. look here.

To be able to load Nuxt modules (the ones which are mixing CJS and ESM syntax) Nuxt is replacing the require function. In CJS module require is just another variable, one can overwrite it and nobody will complain. For example, in Node we cannot require ESM modules, but Nuxt’s require allows this. It checks the source and transforms on demand. Internally Nuxt consumes everything as CJS although it feels like you are feeding it with ESM.

All works smooth until it does not. The lines which I was referencing are detecting the Jest’s global object. If Jest is present Nuxt does not use its require function anymore. Jest’s require is used. Hm.. On surface both of them are require functions, but they are not interchangeable. As I was explaining, Jest is following Node specification and it simply cannot load Nuxt modules.

Same example, one can require ESM module from inside Nuxt. It works, because everything is transformed into CJS. Jest’s require will throw, because ESM modules cannot be required, they must be imported.

Here is an idea for another workaround. Run Jest in ESM mode and include injectGlobals: false in your Jest config. There is no Jest global anymore, Nuxt cannot detect it and is using its own require. Nuxt modules are loaded as expect, but there is another problem – the Test utils depend on Jest’s globals.

If your goal is to e2e test a web app in a browser, Playwright Test Runner is better choice. If your app runs with nuxt start, you will have no trouble to test it.

If you are testing Nuxt module, Jest with Test Utils is better choice. At some point you might want to mock some dependancy (Jest will do the job), or to dig into Nuxt internals (the utils will help).

As always – choose the right tool for the job.

Hope this helps. Happy testing.

mrazauskas avatar Sep 22 '21 08:09 mrazauskas

Also keep in mind that from very beginning Nuxt Test Utils were made to help testing Nuxt modules. @mrazauskas

So nuxt test utils with browser:true is using playwright but not with their runner I guess.

I hoped I could use setupTest in combination with the playwright test runner but this isnt is working as expected. So I think this brings up a lot of confusion for newcomers.

Whats bothering me is that the e2e:setupTest are taking a lot of time to finish. Will try with playwright test runner again.

regenrek avatar Sep 22 '21 20:09 regenrek

@regenrek Thanks for note. I added couple of lines to my previous comment.

Yes, Playwright can be used with Jest too. This was the only way to test with Playwright for some time, but they released own test runner rather recently.

Each time called setupTest will build your app (or fixture) from scratch. That’s the slowest part of a test. In some cases it is a must, in other cases this is unnecessary overhead. In general e2e are slow ones by nature.

mrazauskas avatar Sep 23 '21 04:09 mrazauskas