Cover image by Quinton Coetzee on Unsplash
Recently I've been working on a new project with Expo and React Native 🙂
I want to test my Expo project with Jest. Basically, you can follow Testing with Jest - Expo Documentation, but in the case of TypeScript, you have to change some settings in following points:
First, let me show what the version. of Expo in my machine.
$ expo --version
4.0.17
$ npm list expo
my-expo-app
└── expo@40.0.0
Configuring Jest and add a simple test
First of all, install the necessary packages; it is the Expo way to install jest-expo instead of the jest package.
$ npm i jest-expo react-test-renderer --save-dev
Since it is a TypeScript project, we should install .d.ts
of the package too.
$ npm i @types/jest @types/react-test-renderer --save-dev
Add the Jest configuration to package.json
as mentioned in the documentation.
{
...
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
]
}
}
Notice transformIgnorePatterns
contains a long regular expression 🤔
Jest doesn't transpile files under the node_modules
directory by default, but there are some React Native libraries that are distributed without being transpiled. By setting transformIgnorePatterns
as above, we can make them the target of the transpile.
-
x(?!y)
Negative lookahead assertion: Matches "x" only if "x" is not followed by "y". Such long regular expressions should be commented like Perl, but unfortunately the/x
modifier is not available in JavaScript. 1
Now, let's write a sample test App.test.tsx
.
import React from "react";
import renderer from "react-test-renderer";
import App from "./App";
describe("<App />", () => {
it("has 1 child", () => {
const tree = renderer.create(<App />).toJSON();
// @ts-ignore
expect(tree.children.length).toBe(1);
});
});
In fact, the above code can't be compiled by a type error, but here I've ignored it with @ts-ignore
. This is because fixing the type error is out of the scope of this article, and in the future I will use the React Testing Library instead of using react-test-renderer directly.
Running test and configuring Jest properly
Okay, let's run the test.
$ npx jest
FAIL ./App.test.tsx
<App />
✕ has 1 child (45ms)
● <App /> › has 1 child
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at createFiberFromTypeAndProps (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:16180:21)
at createFiberFromElement (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:16208:15)
at reconcileSingleElement (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5358:23)
at reconcileChildFibers (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5418:35)
at reconcileChildren (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7991:28)
at updateHostRoot (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:8547:5)
at beginWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9994:14)
at performUnitOfWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13800:12)
at workLoopSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13728:5)
at renderRootSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13691:7)
console.error
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
6 | describe("<App />", () => {
7 | it("has 1 child", () => {
> 8 | const tree = renderer.create(<App />).toJSON();
| ^
9 | expect(tree.children.length).toBe(1);
10 | });
11 | });
at printWarning (node_modules/react/cjs/react.development.js:315:30)
at error (node_modules/react/cjs/react.development.js:287:5)
at Object.createElementWithValidation [as createElement] (node_modules/react/cjs/react.development.js:1788:7)
at Object.<anonymous> (App.test.tsx:8:34)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.254s
Ran all test suites.
It failed. The error is also mysterious.
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
Getting a warning that the App
component is an object
. When I run Jest, I get this error on the line below.
import App from "./App";
The error was happened because the import
loads app.json
instead of App.tsx
. To prevent this, configure Jest's moduleFileExtensions
to prioritize .tsx
and .ts
over .json
when loading modules.
"jest": {
...
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx"
]
},
After making this change, the test will pass successfully.
$ npx jest
PASS ./App.test.tsx
<App />
✓ has 1 child (2341ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.855s
Googled and found that someone has the same issue 2, so I posted a PR to the documentation.
Writing Jest configuration file with TypeScript
Okay, I was able to run the test, but the configuration became a bit complicated.
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx"
]
}
In particular, transformIgnorePatterns
is complex, and I'd rather take it out than put it in package.json
. And I want to write it with TypeScript if possible.
From Jest version 26.6.0, TypeScript configuration file is supported. ts-node is required and must be installed.
$ npm i ts-node --save-dev
If the jest which jest-expo depends on is old, install it as well.
$ npm list jest
my-expo-app
└─┬ jest-expo@40.0.1
└── jest@25.5.4
$ npm i jest --save-dev
Then create a new jest.config.ts
and migrate the configuration in package.json
import { Config } from "@jest/types";
// By default, all files inside `node_modules` are not transformed. But some 3rd party
// modules are published as untranspiled, Jest will not understand the code in these modules.
// To overcome this, exclude these modules in the ignore pattern.
const untranspiledModulePatterns = [
"(jest-)?react-native",
"@react-native-community",
"expo(nent)?",
"@expo(nent)?/.*",
"react-navigation",
"@react-navigation/.*",
"@unimodules/.*",
"unimodules",
"sentry-expo",
"native-base",
"react-native-svg",
];
const config: Config.InitialOptions = {
preset: "jest-expo",
transformIgnorePatterns: [
`node_modules/(?!${untranspiledModulePatterns.join("|")})`,
],
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
};
export default config;
Simplifing the lengthy regex expression made things look a lot better. Compared to package.json
, which doesn't allow comments, it's nice to be able to write comments😅.
[2021.01.24] Actually, transformIgnorePatterns
is included in the preset of jest-expo, so there is no need to specify them if you are fine with the default. However, since this list needs to be added depending on the library used, I personally use this method. The above example has been updated with the latest list.
One last point: if you re-installed jest instead of the version that the jest-expo depends on, you will have two versions of jest mixed under your node_modules
.
Check it with the npm list
command.
$ npm list jest
my-expo-app
├── jest@26.6.3
└─┬ jest-expo@40.0.1
└── jest@25.5.4
You can see that there are two versions of Jest. There is one problem with this: we don't know which version the npx command will execute.
The npx command executes commands placed under node_modules/.bin
. These are symbolic links to files in each package, and they will be overwritten depending on the order of installation.
For example, ./node_modules/.bin/jest
is now pointing to . /node_modules/.bin/jest/bin/jest.js
.
$ ls -l ./node_modules/.bin/jest
lrwxr-xr-x 1 takanori_is staff 19 1 16 16:31 ./node_modules/.bin/jest -> ../jest/bin/jest.js
However, if you re-install jest-expo
, ./node_modules/jest-expo/bin/jest.js
will replace it.
$ ls -l ./node_modules/.bin/jest
lrwxr-xr-x 1 takanori_is staff 24 1 16 16:34 ./node_modules/.bin/jest -> ../jest-expo/bin/jest.js
So, until the version of jest
on which jest-expo
depends is upgraded, it is safe to run . /node_modules/jest/bin/jest.js
directly instead of via npx command. Let's register it as a script in package.json
.
"scripts": {
...
"test": "./node_modules/jest/bin/jest.js"
}
You can now run it with npm test
.
$ npm test
> @ test /Users/ishikawasonkyou/Developer/Workspace/my-expo-app
> ./node_modules/jest/bin/jest.js
PASS ./App.test.tsx
<App />
✓ has 1 child (150 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.767 s, estimated 1 s
Ran all test suites.
Top comments (0)