Testing Expo apps with Jest in TypeScript

# Testing Expo apps with Jest in TypeScript

2021/01/16 8:57am

• expo
• reactnative
• typescript
• jest

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": [
]
}
}


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](https://testing-library.com/docs/react-testing-library/ intro/) 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": [
],
"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.