Global variables are the worst thing in programming, but they are common in test runners due to history. Before the invention of bundlers, we used to include the test runner's script before the spec file to make it run.
1 | <!-- or qunit or custom test runner script --> |
The vendor/mocha.js
would set up global variables like describe
and it
, and the tests/my-spec.js
would use them to define and execute tests. In addition to the test functions, there would be other global variables, like Chai's expect
and assert
functions.
Both Cypress and Jest provide global variables like describe
and expect
for your tests to use. The global variables do not clash at runtime. If you are writing a unit test, you execute it with Jest. If you are writing a unit or end-to-end test, you execute it with Cypress. The clashing variables do conflict during the static type checking though. The TypeScript lint step breaks complaining about double registration of variables.
1 | node_modules/@types/jest/index.d.ts:41:13 - error TS2403: Subsequent variable declarations must have the same type. Variable 'it' must be of type 'TestFunction', but here has type 'It'. |
The above messages are from cypress-io/cypress-and-jest-typescript-example.
Workarounds
We suggest solving the type clashes by either skipping checking the 3rd party types in the dependencies using skipLibCheck
TS config option:
1 | { |
Another solution is to explicitly list the types to load when linting; skipping Jest types for example when linting Cypress tests
1 | { |
Of course both workarounds have their limits. We do want to check our types, and listing only the types to use is a maintenance nightmare. What can we do instead?
Ava's example
If you ever used Ava test runner you might appreciate its lack of global variables. Every spec file has to import the test
value explicitly.
1 | const test = require('ava'); |
Nice - no global variables or types. Can we do the same for Cypress tests?
Local Cypress
Cypress test runner includes everything one needs to write tests. Thus there are three global variable types:
- the Cypress' own objects with commands
cy
andCypress
- the test definitions like
describe
andit
coming from Mocha - the assertions like
expect
andassert
from bundled Chai libray
Whenever one uses Cypress types (either by including it in the tsconfig.json
file or by using the special comment line /// <reference types="cypress">
in the spec file) the types are described by the cypress/types/index.d.ts file.
This file just "assembles" the types by including types for Cypress and all its bundled tools.
If we want to shift Cypress from providing global types to having explicit local types, we would need to modify those three type files.
Local cy and Cypress objects
Let's start with cy
and Cypress
definitions. They are declared in the included file cypress-global-vars.d.ts
.
This file has nothing else, so let's "disable" the global variable types cy
and Cypress
by ... commenting out the reference line loading cypress-global-vars.d.ts
from index.d.ts
1 | ... |
We could write a script to do this automatically - I have even created an NPM package called local-cypress that does this during postinstall
step. Just run npm i -D local-cypress
or yarn add -D local-cypress postinstall-postinstall
in your repo, and after every installation it will patch the node_modules/cypress/types/index.d.ts
file if necessary.
If the cy
and Cypress
are no longer declared global (they are still ARE global variables at run-time), how do our specs "know" about them? We need to declare a type for local variables to be imported by our specs. Thus the package local-cypress
defined "dummy" exports with the right types.
1 | /// <reference types="cypress" /> |
Great - the spec can import cy
and Cypress
objects from local-cypress
and get the right typings without globals. Here is a video of using it from the code editor
Local Chai and Mocha
Similarly, the local-cypress
comments out global Chai and Mocha declarations after install. Instead local-cypress
provides locally exported definitions of all functions a typical spec might use.
1 | import {cy, expect, describe, it} from 'local-cypress' |
You can find the full example in repo bahmutov/local-cypress-and-jest-typescript-example that also shows TypeScript linting without any special workarounds; no need to skip the library check, no need to filter the list of types to load.
Note that Jest still brings its own global describe
, it
, expect
and other definitions. Thus be sure to explicitly import them into your tests from local-cypress
to get the right definitions
For example, importing the Cypress' it
declaration from local-cypress
allows you to pass per-test configuration.
Future work
I intend to refactor the Cypress type files a little to make skipping global type declarations simpler - there is still room for improvement. While removing all global declarations in favor of locally defined variables is not on the immediate road map, I hope local-cypress
proves that it is possible and useful.
What about Jest globals? Well, you could ask the Jest development team to deprecate and remove global variables and types.