Quantcast
Channel: Active questions tagged react-native+typescript - Stack Overflow
Viewing all articles
Browse latest Browse all 6287

How to properly configure jest unit and component tests in React Native monorepo with TypeScript

$
0
0

So, my issue is a bit difficult to descibe because our repository (which is at work, so I unfortunately can't just provide a link to it) is pretty big and complex, but I'll do my best. First of all, the problem:

The Problem

I run jest and the result is something like the following:

FAIL  packages/components/src/PlusMinus/index.test.tsx● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1)Test Suites: 1 failed, 1 total

The error message is the same for all tests of files that import anything from inside our monorepo (via @packages/ alias) – incl. the exact same component, IconWithBadge that apparently can't import the Metrics module. See below for the complete output of all our current test suites.

PASS  packages/package1/src/utils/index.test.ts  Testing *****function()√ Normal items (5 ms)√ Other items (2 ms)√ Combo items (5 ms)  Testing *****function()√ Multiple identical items (1 ms)√ Multiple different items, one theme each (1 ms)√ Multiple different items, with a unique theme for each entry (3 ms) PASS  packages/utils/src/transformations/transformations.test.ts  test transformation helpers√ spliceImmutably() (4 ms)√ momentToShortenedDateString() (4 ms)√ sortByProperty() (3 ms)√ roundByPrecision() (1 ms)√ parseDotNotation()√ versionStringToArray() (1 ms)√ capitalizeString() (1 ms)√ normalizeString() (2 ms)√ normalizeUmlauts1() (3 ms)√ normalizeUmlauts2() (2 ms) FAIL  packages/utils/src/validation/validation.test.ts● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL  packages/customer/src/screens/Profile/components/CustomerCard/components/PinInput/index.test.tsx● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL  packages/package2/src/screens/Screen1/components/Component1/index.test.tsx● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL  packages/components/src/PlusMinus/index.test.tsx● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL  packages/package3/src/SomeService/utils/index.test.ts● Test suite failed to run    TypeError: Cannot read properties of undefined (reading 'spacing')      15 |     justifyContent: 'center',      16 |     alignItems: 'center',> 17 |     paddingRight: Metrics.spacing.xxs,         |                           ^      18 |   },      19 | });      20 |      at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27)      at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1)Test Suites: 5 failed, 2 passed, 7 totalTests:       16 passed, 16 totalSnapshots:   0 totalTime:        8.832 s

It seems relatively obvious to me that this must be some kind of compilation error because the important thing to note here is:

In a React Native context all of this code works perfectly fine!

Meaning, when building and and running the app via react-native commands, like usual, everything works and I don't get any errors. It's only in the context of jest testing that I get this error.

Also, let's say, I remove all instances of Metrics from IconWithBadge and run jest again. Then, it will find another file that imports Metrics where it's miraculously undefined.

Now, to give you an overview of how our repository is structured and configured, let's have a look at the environment.

The Environment

We have a monorepo with a structure similar to this:

.├── app-directory│├── android│├── ios│├── src││├── navigation│││└── ... all the react-navigation navigators││└── App.tsx│├── babel.config.js│├── index.js│├── metro.config.js│├── package.json│├── react-native.config.js│└── tsconfig.json├── e2e│├── tests││└── testSomething.e2e.ts│├── config.json│└── environment.js├── packages│├── package1││├── src││├── package.json││├── tsconfig.build.json││└── tsconfig.json│├── package2││├── src││├── package.json││├── tsconfig.build.json││└── tsconfig.json│└── ...more packages, all look similar├── .eslintignore├── .eslintrc.yml├── .gitattributes├── .gitignore├── .babel.config.js├── detox.config.js├── get-babel-config.js├── jest.config.js├── package.json├── tsconfig.app.json├── tsconfig.json├── tsconfig.spec.json└── yarn.lock

We have both component tests (via jest-native and RN Testing Library) and plain unit tests inside of the repo. We also have detox tests, as you can deduce from the existence of an e2e directory, but that's not important at the moment.

All code (test and app code) is written in TypeScript, and for the React Native part it's all compiled via Babel, as is recommended by the RN team. For the tests however, I have configured ts-jest to compile the code to CommonJS, which is required by jest. It is specifically configured to compile the TypeScript code with ttypescript and use babel-jest for all JavaScript code.

To show you how exactly it is all configured, I'll paste my most important configs here:

The Configs

Jest Config

Since we use Expo's unimodules, we also have jest-expo installed, which sets up the Expo-side of jest-testing properly. With this and ts-jest in mind, our jest.config.js looks like this:

const cloneDeep = require('lodash/cloneDeep');const expoPreset = cloneDeep(require('jest-expo/jest-preset'));const tsjPreset = cloneDeep(require('ts-jest/presets').jsWithBabel);delete expoPreset.transform['^.+\\.(js|ts|tsx)$'];/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */const config = {  bail: true,  globals: {'ts-jest': {      compiler: 'ttypescript',      tsconfig: 'tsconfig.spec.json',      isolatedModules: true,      diagnostics: {        warnOnly: true,      },    },  },  moduleDirectories: ['node_modules',    // add the directory with the test-utils.js file, for example:'test-utils', // a utility folder'packages', // packages folder    __dirname, // the root directory  ],  moduleFileExtensions: ['ts', 'tsx', 'js'],  moduleNameMapper: {'^@packages/utils/(.*)$': '<rootDir>/packages/utils/src/$1','^@packages/services/(.*)$': '<rootDir>/packages/services/src/$1','^@packages/(.*)$': '<rootDir>/packages/$1/src','^test-utils$': '<rootDir>/test-utils',  },  rootDir: process.cwd(),  setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],  testTimeout: 10000,  transform: Object.assign(tsjPreset.transform, expoPreset.transform),  transformIgnorePatterns: [    `${      expoPreset.transformIgnorePatterns[0].split('svg)')[0]    }svg|@packages/*|react-native-ultimate-config|react-native-calendars|react-native-permissions|react-native-swipe-gestures|react-native-iphone-x-helper|react-native-webview|react-native-reanimated|react-native-qrcode-svg|react-native-linear-gradient|@react-native-masked-view/*|react-native-svg-path-gradient|react-native-safe-area-context|redux-middleware-flipper|react-native-flipper)`,  ],  verbose: true,};module.exports = Object.assign(expoPreset, Object.assign(tsjPreset, config));

Babel config

./get-babel-config.js

/** * @param {import('@types/babel__core').ConfigAPI} api * @param {boolean} isRoot * @returns {import('@types/babel__core').TransformOptions} */module.exports = function (api, isRoot = true) {  const relativeRoot = isRoot ? './' : '../';  /** @type {import('@types/babel__core').TransformOptions} */  const config = {    presets: [      ['@babel/preset-env', {targets: 'defaults', loose: true}],      ['module:metro-react-native-babel-preset', {useTransformReactJSXExperimental: true}],    ],    sourceMaps: 'inline',  };  config.plugins = ['lodash',    ['module-resolver',      {        root: [relativeRoot],        extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],        alias: {'^@packages/services/(.+)': `${relativeRoot}packages/services/src/\\1`,'^@packages/utils/(.+)': `${relativeRoot}packages/utils/src/\\1`,'^@packages/(.+)': `${relativeRoot}packages/\\1/src`,'^test-utils': `${relativeRoot}test-utils`,        },      },    ],    ['@babel/plugin-transform-react-jsx', {runtime: 'automatic'}],  ];  if (api.env('production')) {    config.plugins.push(['transform-remove-console',      {exclude: ['error', 'warn', 'info', 'time', 'timeEnd']},    ]);  }  return config;};

./app-directory/babel.config.js

const getConfig = require('../get-babel-config');module.exports = function (api) {  return getConfig(api, false);};

./babel.config.js

const getConfig = require('./get-babel-config');module.exports = function (api) {  return getConfig(api);};

TypeScript Config

./tsconfig.json

{"compilerOptions": {"target": "ES6","module": "ES6","allowJs": false,"jsx": "react-native","resolveJsonModule": true,"declaration": true,"declarationMap": true,"sourceMap": true,"composite": true,"importHelpers": true,"isolatedModules": true,"strict": true,"noImplicitAny": true,"strictNullChecks": true,"strictFunctionTypes": true,"strictPropertyInitialization": true,"skipLibCheck": true,"noImplicitThis": true,"alwaysStrict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noImplicitReturns": true,"forceConsistentCasingInFileNames": true,"moduleResolution": "node","baseUrl": ".","paths": {"@packages/*": ["./packages/*/src"      ],"@packages/services/*": ["./packages/services/src/*"      ],"@packages/utils/*": ["./packages/utils/src/*"      ]    },"types": ["node","jest","detox"    ],"allowSyntheticDefaultImports": true,"esModuleInterop": true,"plugins": [      {"transform": "@zerollup/ts-transform-paths"      },      {"transform": "typescript-transform-react-jsx-source"      }    ]  },"exclude": ["node_modules","**/*.js"  ]}

./tsconfig.spec.json

{"extends": "./tsconfig.json","compilerOptions": {"target": "ES5","jsx": "react-jsx","paths": {"@packages/*": ["./packages/*/src"      ],"@packages/services/*": ["./packages/services/src/*"      ],"@packages/utils/*": ["./packages/utils/src/*"      ],"test-utils": ["./test-utils"      ]    }  }}

Packacke JSON

./package.json

{"private": true,"workspaces": {"packages": ["packages/*","app-directory"    ]  },"license": "MIT","scripts": {"lint": "eslint \"**/*.{js,ts,tsx}\"","typescript": "tsc","test:jest": "jest","test:jest:plusminus-only": "jest ./packages/components/src/PlusMinus/index.test.tsx","test:detox-ios": "detox build -c ios && detox test -c ios -l verbose","test:detox-ios-ci": "detox build -c ios && detox test -c ios --workers 2 --headless --record-logs all --cleanup","test:detox-android": "detox build -c android && detox test -c android -l verbose --record-logs all","test:detox-android-ci": "detox build -c android && detox test -c android --workers 2 --headless --record-logs all --cleanup","publish": "lerna publish","prerelease": "lerna run clean","in-app": "yarn --cwd app-directory","postinstall": "patch-package"  },"devDependencies": {"@babel/cli": "^7.15.4","@babel/core": "^7.15.5","@babel/preset-env": "^7.15.6","@babel/runtime": "^7.15.4","@packages/build-tools": "^1.0.31","@react-native-community/eslint-config": "^3.0.1","@testing-library/jest-native": "^4.0.2","@testing-library/react-native": "^7.2.0","@types/detox": "^17.14.2","@types/i18n-js": "^3.8.2","@types/jest": "^27.0.1","@types/lodash": "^4.14.173","@types/node": "^16.9.2","@types/react": "^17.0.21","@types/react-native": "^0.65.0","@types/react-native-calendars": "^1.1264.2","@types/react-native-vector-icons": "^6.4.8","@types/react-test-renderer": "^17.0.1","@types/react-redux": "^7.1.18","@typescript-eslint/eslint-plugin": "^4.31.1","@typescript-eslint/parser": "^4.31.1","@zerollup/ts-transform-paths": "^1.7.18","babel-jest": "^27.2.0","babel-plugin-lodash": "^3.3.4","babel-plugin-module-resolver": "^4.1.0","babel-plugin-transform-remove-console": "^6.9.4","detox": "^18.20.3","eslint": "^7.32.0","eslint-config-prettier": "^8.3.0","eslint-plugin-detox": "^1.0.0","eslint-plugin-import": "^2.24.2","eslint-plugin-jest": "^24.4.2","eslint-plugin-prettier": "^4.0.0","eslint-plugin-testing-library": "^4.12.2","jest": "^27.2.0","jest-expo": "^42.1.0","lerna": "^4.0.0","metro-react-native-babel-preset": "^0.66.2","patch-package": "^6.4.7","postinstall-postinstall": "^2.1.0","prettier": "^2.4.1","react-native-codegen": "^0.0.7","react-test-renderer": "^17.0.2","run-script-os": "^1.1.6","sanitize-filename": "^1.6.3","ts-jest": "^27.0.5","tslib": "^2.3.1","ttypescript": "^1.5.12","typescript": "^4.4.3","typescript-transform-react-jsx-source": "^2.0.0"  },"prettier": {"tabWidth": 2,"bracketSpacing": false,"jsxBracketSameLine": true,"singleQuote": true,"trailingComma": "all","printWidth": 100,"endOfLine": "lf"  },"resolutions": {"jest-expo/babel-jest": "^27.2.0","jest-expo/jest": "^27.2.0","jest-expo/react-test-renderer": "^17.0.2"  }}

./app-directory/package.json

{"private": true,"scripts": {"start-server": "react-native start --reset-cache","doctor": "react-native doctor","run-app": "react-native run-android --variant=devdemoDebug","run-app:release": "react-native run-android --variant=devdemoRelease","install-pods": "rm -rf ios/Pods && cd ios && pod install --clean-install && cd ..","test": "jest --passWithNoTests"  },"dependencies": {"@packages/authentication": "^1.0.31","@packages/package1": "^1.0.31","@packages/package2": "^1.0.31","@packages/components": "^1.0.31","@packages/customer": "^1.0.31","@packages/errors": "^1.0.31","@packages/package3": "^1.0.31","@packages/package4": "^1.0.31","@packages/package5": "^1.0.31","@packages/hooks": "^1.0.31","@packages/package6": "^1.0.31","@packages/package7": "^1.0.31","@packages/package8": "^1.0.31","@packages/screens": "^1.0.31","@packages/services": "^1.0.31","@packages/store": "^1.0.31","@packages/themes": "^1.0.31","@packages/utils": "^1.0.31","@packages/package9": "^1.0.31","@packages/package10": "^1.0.31","@ctrl/tinycolor": "^3.4.0","@react-native-async-storage/async-storage": "^1.15.8","@react-native-clipboard/clipboard": "^1.8.4","@react-native-community/blur": "^3.6.0","@react-native-community/netinfo": "^6.0.2","@react-native-community/viewpager": "4.x","@react-native-masked-view/masked-view": "^0.2.6","@react-navigation/bottom-tabs": "^5.11.15","@react-navigation/drawer": "^5.12.9","@react-navigation/native": "^5.9.8","@react-navigation/stack": "^5.14.9","@reduxjs/toolkit": "^1.6.1","expo-barcode-scanner": "^10.2.2","expo-local-authentication": "^11.1.1","lottie-ios": "3.2.3","lottie-react-native": "^4.0.3","moment": "^2.29.1","react": "^17.0.2","react-native": "^0.65.1","react-native-bootsplash": "^3.2.5","react-native-camera": "3.x","react-native-console-time-polyfill": "^1.2.3","react-native-device-info": "^8.3.3","react-native-exception-handler": "^2.10.10","react-native-flipper": "^0.109.0","react-native-gesture-handler": "^1.10.3","react-native-inappbrowser-reborn": "^3.6.3","react-native-keychain": "^7.0.0","react-native-linear-gradient": "^2.5.6","react-native-localize": "^2.1.4","react-native-permissions": "^3.0.5","react-native-reanimated": "^1.13.3","react-native-restart": "^0.0.22","react-native-safe-area-context": "^3.3.2","react-native-screens": "^3.7.2","react-native-share": "^7.1.0","react-native-simple-toast": "^1.1.3","react-native-svg": "^12.1.1","react-native-ultimate-config": "^3.4.1","react-native-unimodules": "^0.14.8","react-native-url-polyfill": "^1.3.0","react-native-vector-icons": "7.0.0","react-native-view-shot": "^3.1.2","react-native-wallet-passes": "^1.2.2","react-native-webview": "11.13.0","react-redux": "^7.2.5","redux": "^4.1.1","rn-fetch-blob": "^0.12.0"  },"devDependencies": {"@packages/types-and-enums": "^1.0.31"  }}

Summary

This should be all the pertinent information. If there's anything missing/you need additional info on anything, let me know! To sum up the question:

Why is jest not able to resolve this package when it works totally fine during normal development/production? How might I be able to solve this problem?

Asking all jest experts here! 😅 I hope you can help me.


Viewing all articles
Browse latest Browse all 6287

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>