代码转换

Jest 会在项目里以原始的 JavaScript 执行,所以如果你用了一些 Node 环境不支持的语法 (比如 JSX, TypeScript, Vue 模板语法),那就要把你的代码转译成原始的 JavaScript,这就跟你在构建浏览器前端代码时要做的转译工作一样。

Jest 提供 transform 配置来支持 Js 转译。

转译器(Transformer) 是一个能提供转译源代码能力的模块。 举个例子,假如你想在你的业务和测试代码中使用一些还没被 Node 支持的新语言特性,你可以引入一个代码预处理器来将新版本的 JavaScript 转译成当前支持的版本。

Jest将会缓存转换后的结果,并且试图让多方因素(比如正在转换的文件源和配置信息被修改等)造成的结果无效

默认值

Jest 已经内置了一个现成的转译器 —— –babel-jest。 它会加载你项目的 Babel 配置,然后转译所有能正确匹配 /\.[jt]sx?$/ 正则表达式的文件(也即所有 .js.jsx.ts.tsx 文件)。 此外,babel-jest还将会注入 ES Module mocking中所提到的Babel插件。

代码转换 - 图1tip

记住,如果你想将它和其他代码预处理器一起使用,那必须要显式地引入默认的 babel-jest 转译器,

  1. "transform": {
  2. "\\.[jt]sx?$": "babel-jest",
  3. "\\.css$": "some-css-transformer",
  4. }

编写自定义Transformers

你可以编写专属的Transformer, Transformer的API如下所示:

  1. interface TransformOptions<TransformerConfig = unknown> {
  2. supportsDynamicImport: boolean;
  3. supportsExportNamespaceFrom: boolean;
  4. supportsStaticESM: boolean;
  5. supportsTopLevelAwait: boolean;
  6. instrument: boolean;
  7. /** Cached file system which is used by `jest-runtime` to improve performance. */
  8. cacheFS: Map<string, string>;
  9. /** Jest configuration of currently running project. */
  10. config: ProjectConfig;
  11. /** Stringified version of the `config` - useful in cache busting. */
  12. configString: string;
  13. /** Transformer configuration passed through `transform` option by the user. */
  14. transformerConfig: TransformerConfig;
  15. }
  16. type TransformedSource = {
  17. code: string;
  18. map?: RawSourceMap | string | null;
  19. };
  20. interface SyncTransformer<TransformerConfig = unknown> {
  21. canInstrument?: boolean;
  22. getCacheKey?: (
  23. sourceText: string,
  24. sourcePath: string,
  25. options: TransformOptions<TransformerConfig>,
  26. ) => string;
  27. getCacheKeyAsync?: (
  28. sourceText: string,
  29. sourcePath: string,
  30. options: TransformOptions<TransformerConfig>,
  31. ) => Promise<string>;
  32. process: (
  33. sourceText: string,
  34. sourcePath: string,
  35. options: TransformOptions<TransformerConfig>,
  36. ) => TransformedSource;
  37. processAsync?: (
  38. sourceText: string,
  39. sourcePath: string,
  40. options: TransformOptions<TransformerConfig>,
  41. ) => Promise<TransformedSource>;
  42. }
  43. interface AsyncTransformer<TransformerConfig = unknown> {
  44. canInstrument?: boolean;
  45. getCacheKey?: (
  46. sourceText: string,
  47. sourcePath: string,
  48. options: TransformOptions<TransformerConfig>,
  49. ) => string;
  50. getCacheKeyAsync?: (
  51. sourceText: string,
  52. sourcePath: string,
  53. options: TransformOptions<TransformerConfig>,
  54. ) => Promise<string>;
  55. process?: (
  56. sourceText: string,
  57. sourcePath: string,
  58. options: TransformOptions<TransformerConfig>,
  59. ) => TransformedSource;
  60. processAsync: (
  61. sourceText: string,
  62. sourcePath: string,
  63. options: TransformOptions<TransformerConfig>,
  64. ) => Promise<TransformedSource>;
  65. }
  66. type Transformer<TransformerConfig = unknown> =
  67. | SyncTransformer<TransformerConfig>
  68. | AsyncTransformer<TransformerConfig>;
  69. type TransformerCreator<
  70. X extends Transformer<TransformerConfig>,
  71. TransformerConfig = unknown,
  72. > = (transformerConfig?: TransformerConfig) => X;
  73. type TransformerFactory<X extends Transformer> = {
  74. createTransformer: TransformerCreator<X>;
  75. };

代码转换 - 图2note

为了简洁起见,上面某些类型没有完全列出。 完整代码可以看 Github 上的 Jest 仓库(记得要对你当前的 Jest 版本选择正确的 tag/commit)

使用 Jest 时,有几种引入模块的方式 - 使用 Common JS (require) 或者 ECMAScript Modules (import -静态和动态引入) Jest 会按需把文件传给转译器 (比如,当检测到 requireimport 的时候) 这个过程也称为 “转译”,可能是同步 (使用require 的时候) 进行的,也可能是异步进行的 (使用 importimport() 的时候,后者也适用于 Common JS 模块) 因此,这个接口也暴露了异步和同步过程的两对方法: process{Async}getCacheKey{Async} 后者也可以用来检查我们是否真的需要调用 process{Async} 由于异步转译可以在没有问题的情况下同步进行,所以异步情况可能会“降级”为同步进行,但是反过来就不行了

So if your code base is ESM only implementing the async variants is sufficient. Otherwise, if any code is loaded through require (including createRequire from within ESM), then you need to implement the synchronous variant. Be aware that node_modules is not transpiled with default config.

Semi-related to this are the supports flags we pass (see CallerTransformOptions above), but those should be used within the transform to figure out if it should return ESM or CJS, and has no direct bearing on sync vs async

Though not required, we highly recommend implementing getCacheKey as well, so we do not waste resources transpiling when we could have read its previous result from disk. 你还可以使用@jest/create-cache-key-function来帮助你实现它

Instead of having your custom transformer implement the Transformer interface directly, you can choose to export createTransformer, a factory function to dynamically create transformers. This is to allow having a transformer config in your jest config.

需要注意的是 ECMAScript module的支持是由supports* 选项指明的。 具体来说supportsDynamicImport: true 表示的是这个transformer可以返回ESM和CJS都支持的import()表达式。 而 supportsStaticESM: true 则表示的是支持最高级别的import语句,代码将被解释为ESM而不是CJS。 阅读 Node’s docs了解ESM和CJS之间的具体差异信息

代码转换 - 图3tip

Make sure process{Async} method returns source map alongside with transformed code, so it is possible to report line information accurately in code coverage and test errors. Inline source maps also work but are slower.

During the development of a transformer it can be useful to run Jest with --no-cache to frequently delete cache.

例子

检查带有类型的TypeScript

babel-jest默认情况下会传输TypeScript文件,但Babel并不会对类型校验。 如果你需要校验类型你可以使用 ts-jest.

将图片转换为其路径

导入图像是将其包含在浏览器包中的一种方法,但它们不是有效的JavaScript。 在Jest中有一种解决方法是将它们的文件名替换成导入值

fileTransformer.js

  1. const path = require('path');
  2. module.exports = {
  3. process(sourceText, sourcePath, options) {
  4. return {
  5. code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
  6. };
  7. },
  8. };

jest.config.js

  1. module.exports = {
  2. transform: {
  3. '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
  4. '<rootDir>/fileTransformer.js',
  5. },
  6. };