Angular7.2 に Jest を導入してみる

## 概要
**Angular7**で、Jestを試しに導入したときのメモです。
サイトによっては、大体4コマンドだけ打てば動くみたいな書き方がしてあるが、
実際そうではないみたいで、試行錯誤で対処したものを参考にまとめる。

導入の仕方は2通りある。

* A. もう作ってある Karma環境を `@briebug/jest-schematic` で変換する
* B. パッケージがないところから、環境を作る  
  A.のついでに、試して動いたので手順を一応メモを残す。

**環境**

* Angular CLI: 7.2.4
* Node: 11.13.0
* OS: win32 x64
* Angular: 7.2.15
* Jest
  * `@angular-builders/jest@7.4.4`
  * `jest-preset-angular@8.0.0`
  * `jest@24.9.0`

### A. もう作ってあるKarma環境を変換する

```
npm install -g @briebug/jest-schematic
ng g @briebug/jest-schematic:add
ng add @briebug/jest-schematic
npm i -D @angular-builders/jest@7.4.4
```

#### A.1 package.json の編集
エラーになるみたいで astTransformers の部分を書き換える。

```javascript
  "jest": {
    "preset": "jest-preset-angular",
    "roots": [
      "src"
    ],
    "transform": {
      "^.+\\.(ts|js|html)$": "ts-jest"
    },
    "setupFilesAfterEnv": [
      "<rootDir>/src/setup-jest.ts"
    ],
    "moduleNameMapper": {
      "@app/(.*)": "<rootDir>/src/app/$1",
      "@assets/(.*)": "<rootDir>/src/assets/$1",
      "@core/(.*)": "<rootDir>/src/app/core/$1",
      "@env": "<rootDir>/src/environments/environment",
      "@src/(.*)": "<rootDir>/src/src/$1",
      "@state/(.*)": "<rootDir>/src/app/state/$1"
    },
    "globals": {
      "ts-jest": {
        "tsConfig": "<rootDir>/src/tsconfig.spec.json",
        "stringifyContentPathRegex": "\\.html$",
        "astTransformers": [
          "jest-preset-angular/build/InlineFilesTransformer",
          "jest-preset-angular/build/StripStylesTransformer"
        ]
      }
    }
```

#### A.2 angular.json の編集

```diff
         "test": {
-          "builder": "@angular-devkit/build-angular:karma",
+          "builder": "@angular-builders/jest:run",
           "options": {
             "main": "src/test.ts",
             "polyfills": "src/polyfills.ts",
```

#### A.3 src/jest.config.js の追加
snapshotSerializers の部分に注意。

```javascript
module.exports = {
  "transform": {
      "^.+\\.(ts|js|html)$": "ts-jest",
      "^.+\\.[t|j]sx?$": "babel-jest"
  },
  moduleFileExtensions: ['ts', 'html', 'js', 'json'],
  moduleNameMapper: {
      '^src/(.*)$': '<rootDir>/src/$1',
      '^app/(.*)$': '<rootDir>/src/app/$1',
      '^assets/(.*)$': '<rootDir>/src/assets/$1',
      '^environments/(.*)$': '<rootDir>/src/environments/$1',
  },
  transformIgnorePatterns: ['node_modules/(?!@ngrx)'],
  snapshotSerializers: [
      'jest-preset-angular/build/AngularSnapshotSerializer.js',
      'jest-preset-angular/build/HTMLCommentSerializer.js',
  ],
};
```

#### A.4 src/tsconfig.spec.json の編集

```diff
diff --git a/list-server-nohttp/src/tsconfig.spec.json b/list-server-nohttp/src/tsconfig.spec.json
index de77336..400bd63 100644
--- a/list-server-nohttp/src/tsconfig.spec.json
+++ b/list-server-nohttp/src/tsconfig.spec.json
@@ -3,8 +3,7 @@
   "compilerOptions": {
     "outDir": "../out-tsc/spec",
     "types": [
-      "jasmine",
-      "node"
+      "jest"
     ]
   },
```

### B. パッケージがないところから環境を作る

#### B.1 package.json の編集
package.json を A で実績のあるものを参考に取り込む。

<details><summary>diff package.json</summary><div>

```diff
diff --git a/list-server-nohttp/package.json b/list-server-nohttp/package.json
index 1fbf179..e9b1bfd 100644
--- a/list-server-nohttp/package.json
+++ b/list-server-nohttp/package.json
@@ -5,9 +5,10 @@
     "ng": "ng",
     "start": "ng serve",
     "build": "ng build",
-    "test": "ng test",
+    "test": "jest",
     "lint": "ng lint",
-    "e2e": "ng e2e"
+    "e2e": "ng e2e",
+    "test:watch": "jest --watch"
   },
   "private": true,
   "dependencies": {
@@ -19,12 +20,14 @@
     "@angular/platform-browser": "~7.2.0",
     "@angular/platform-browser-dynamic": "~7.2.0",
     "@angular/router": "~7.2.0",
+    "@briebug/jest-schematic": "^2.1.0",
     "core-js": "^2.5.4",
     "rxjs": "~6.3.3",
     "tslib": "^1.9.0",
     "zone.js": "~0.8.26"
   },
   "devDependencies": {
+    "@angular-builders/jest": "^7.4.4",
     "@angular-devkit/build-angular": "~0.12.0",
     "@angular/cli": "~7.2.4",
     "@angular/compiler-cli": "~7.2.0",
@@ -32,17 +35,45 @@
     "@types/node": "~8.9.4",
     "@types/jasmine": "~2.8.8",
     "@types/jasminewd2": "~2.0.3",
+    "@types/jest": "24.0.21",
     "codelyzer": "~4.5.0",
     "jasmine-core": "~2.99.1",
     "jasmine-spec-reporter": "~4.2.1",
-    "karma": "~3.1.1",
-    "karma-chrome-launcher": "~2.2.0",
-    "karma-coverage-istanbul-reporter": "~2.0.1",
-    "karma-jasmine": "~1.1.2",
-    "karma-jasmine-html-reporter": "^0.2.2",
+    "jest": "24.9.0",
+    "jest-preset-angular": "8.0.0",
     "protractor": "~5.4.0",
     "ts-node": "~7.0.0",
     "tslint": "~5.11.0",
     "typescript": "~3.1.6"
+  },
+  "jest": {
+    "preset": "jest-preset-angular",
+    "roots": [
+      "src"
+    ],
+    "transform": {
+      "^.+\\.(ts|js|html)$": "ts-jest"
+    },
+    "setupFilesAfterEnv": [
+      "<rootDir>/src/setup-jest.ts"
+    ],
+    "moduleNameMapper": {
+      "@app/(.*)": "<rootDir>/src/app/$1",
+      "@assets/(.*)": "<rootDir>/src/assets/$1",
+      "@core/(.*)": "<rootDir>/src/app/core/$1",
+      "@env": "<rootDir>/src/environments/environment",
+      "@src/(.*)": "<rootDir>/src/src/$1",
+      "@state/(.*)": "<rootDir>/src/app/state/$1"
+    },
+    "globals": {
+      "ts-jest": {
+        "tsConfig": "<rootDir>/src/tsconfig.spec.json",
+        "stringifyContentPathRegex": "\\.html$",
+        "astTransformers": [
+          "jest-preset-angular/build/InlineFilesTransformer",
+          "jest-preset-angular/build/StripStylesTransformer"
+        ]
+      }
+    }
   }
 }
```
</div>

#### B.2. angular.json の修正

* angular.json の修正 (Aに同じ)

#### B.3. src/jest.config.js の追加
Aの手順で実績のあるものを参考に取り込む。

<details><summary>src/jest.config.js</summary><div>

```javascript
module.exports = {
  "transform": {
      "^.+\\.(ts|js|html)$": "ts-jest",
      "^.+\\.[t|j]sx?$": "babel-jest"
  },
  moduleFileExtensions: ['ts', 'html', 'js', 'json'],
  moduleNameMapper: {
      '^src/(.*)$': '<rootDir>/src/$1',
      '^app/(.*)$': '<rootDir>/src/app/$1',
      '^assets/(.*)$': '<rootDir>/src/assets/$1',
      '^environments/(.*)$': '<rootDir>/src/environments/$1',
  },
  transformIgnorePatterns: ['node_modules/(?!@ngrx)'],
  snapshotSerializers: [
      'jest-preset-angular/build/AngularSnapshotSerializer.js',
      'jest-preset-angular/build/HTMLCommentSerializer.js',
  ],
  globals: {
    'ts-jest': {
      diagnostics: {
        ignoreCodes: [151001]
      }
    }
  }
};
```
</div>

#### B.4. src/setup-jest.ts の追加
Aの手順で実績のあるものを参考に取り込む。

<details><summary>src/setup-jest.ts</summary><div>

```typescript
import 'jest-preset-angular';

/* global mocks for jsdom */
const mock = () => {
  let storage: { [key: string]: string } = {};
  return {
    getItem: (key: string) => (key in storage ? storage[key] : null),
    setItem: (key: string, value: string) => (storage[key] = value || ''),
    removeItem: (key: string) => delete storage[key],
    clear: () => (storage = {})
  };
};

Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'getComputedStyle', {
  value: () => ['-webkit-appearance'],
});

Object.defineProperty(document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});

/* output shorter and more meaningful Zone error stack traces */
// Error.stackTraceLimit = 2;
```
</div>

#### B.5 src/test-config.helper.ts の追加
Aの手順で実績のあるものを参考に取り込む。

<details><summary>src/test-config.helper.ts</summary><div>

```typescript
import { TestBed } from '@angular/core/testing';

type CompilerOptions = Partial<{
  providers: any[];
  useJit: boolean;
  preserveWhitespaces: boolean;
}>;
export type ConfigureFn = (testBed: typeof TestBed) => void;

export const configureTests = (
  configure: ConfigureFn,
  compilerOptions: CompilerOptions = {}
) => {
  const compilerConfig: CompilerOptions = {
    preserveWhitespaces: false,
    ...compilerOptions,
  };

  const configuredTestBed = TestBed.configureCompiler(compilerConfig);

  configure(configuredTestBed);

  return configuredTestBed.compileComponents().then(() => configuredTestBed);
};
```
</div>

#### B.6 src/tsconfig.spec.json の編集
Aの手順で実績のあるものを参考に取り込む。

```diff
diff --git a/list-server-nohttp/src/tsconfig.spec.json b/list-server-nohttp/src/tsconfig.spec.json
index de77336..400bd63 100644
--- a/list-server-nohttp/src/tsconfig.spec.json
+++ b/list-server-nohttp/src/tsconfig.spec.json
@@ -3,8 +3,7 @@
   "compilerOptions": {
     "outDir": "../out-tsc/spec",
     "types": [
-      "jasmine",
-      "node"
+      "jest"
     ]
   },
```

### Trouble shooting

#### Could not find module "karma"
angular.json を修正していない。

```shell-session
list-server-nohttp> ng test
Could not find module "karma" from "～\\list-server-nohttp".
Error: Could not find module "karma" from "～\\list-server-nohttp".
    at Object.resolve (～\list-server-nohttp\node_modules\@angular-devkit\core\node\resolve.js:141:11)
    at resolveProjectModule (～\list-server-nohttp\node_modules\@angular-devkit\build-angular\src\angular-cli-files\utilities\require-project-module.js:13:19)
    at Object.requireProjectModule (～\list-server-nohttp\node_modules\@angular-devkit\build-angular\src\angular-cli-files\utilities\require-project-module.js:22:20)
    at Observable.rxjs_1.Observable.obs [as _subscribe] (～\list-server-nohttp\node_modules\@angular-devkit\build-angular\src\karma\index.js:28:52)
    at Observable._trySubscribe (～\list-server-nohttp\node_modules\rxjs\internal\Observable.js:44:25)
    at Observable.subscribe (～\list-server-nohttp\node_modules\rxjs\internal\Observable.js:30:22)
    at ～\list-server-nohttp\node_modules\rxjs\internal\util\subscribeTo.js:22:31
    at Object.subscribeToResult (～\list-server-nohttp\node_modules\rxjs\internal\util\subscribeToResult.js:10:45)
    at MergeMapSubscriber._innerSub (～\list-server-nohttp\node_modules\rxjs\internal\operators\mergeMap.js:82:29)
    at MergeMapSubscriber._tryNext (～\list-server-nohttp\node_modules\rxjs\internal\operators\mergeMap.js:76:14)
    at MergeMapSubscriber._next (～\list-server-nohttp\node_modules\rxjs\internal\operators\mergeMap.js:59:18)
    at MergeMapSubscriber.Subscriber.next (～\list-server-nohttp\node_modules\rxjs\internal\Subscriber.js:67:18)
    at Observable._subscribe (～\list-server-nohttp\node_modules\rxjs\internal\observable\scalar.js:6:20)
    at Observable._trySubscribe (～\list-server-nohttp\node_modules\rxjs\internal\Observable.js:44:25)
    at Observable.subscribe (～\list-server-nohttp\node_modules\rxjs\internal\Observable.js:30:22)
    at MergeMapOperator.call (～\list-server-nohttp\node_modules\rxjs\internal\operators\mergeMap.js:39:23)
list-server-nohttp>
```

#### Validation Error: ... node_modules\jest-preset-angular\AngularSnapshotSerializer.js in the snapshotSerializers option was not found.
jest.config.js が存在していない。
ほか、"AngularSnapshotSerializer.js in the snapshotSerializers option was not found." の方は、 `jest-preset-angular` のバージョンが 7系の時にエラーになったりする。この場合、 `npm install -D jest-preset-angular@8` でうまくいくようになるかもしれない。

```
list-server-nohttp> ng test
warning: unable to locate custom jest configuration file at path "～\list-server-nohttp\src\jest.config.js"
● Validation Error:

  Module ～\list-server-nohttp\node_modules\jest-preset-angular\AngularSnapshotSerializer.js in the snapshotSerializers option was not found.
         <rootDir> is: D:\develop\angular\angular7.x-jest\angular-examples\list-server-nohttp

  Configuration Documentation:
  https://jestjs.io/docs/configuration.html

list-server-nohttp>
```

* [javascript - Angular 7 & Jest & Babel Integration over Jasmine/Karma - AngularSnapshotSerializer.js in the snapshotSerializers option was not found - Stack Overflow](https://stackoverflow.com/questions/58608480/angular-7-jest-babel-integration-over-jasmine-karma-angularsnapshotseriali)

#### Validation Error: Module <rootDir>/src/setup-jest.ts in the setupFilesAfterEnv option was not found.
setup-jest.ts が存在していない。

```
list-server-nohttp> ng test
● Validation Error:

  Module <rootDir>/src/setup-jest.ts in the setupFilesAfterEnv option was not found.
         <rootDir> is: ～\list-server-nohttp

  Configuration Documentation:
  https://jestjs.io/docs/configuration.html

list-server-nohttp>
```

### References

* [briebug/jest-schematic: Angular schematic for adding Jest and the required files to an Angular CLI project](https://github.com/briebug/jest-schematic)
* [Snapshot Testing · Jest](https://jestjs.io/docs/en/snapshot-testing)
* [module: esnext triggers the module warning diagnostic · Issue #748 · kulshekhar/ts-jest](https://github.com/kulshekhar/ts-jest/issues/748)
* [Jest with Angular | Anders Skarby](https://blog.skarby.info/jest-with-angular/)  
  typescript toMatchSnapshot fixture import angular jest
* [Tried to introduce Jest on Angular 7 - Tomohiro Uemura - Medium](https://medium.com/@t.uemura618/tried-to-introduce-jest-on-angular-7-angular-7-%E3%81%AB-jest%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F-1d88adaa5d45)
* [Angular CLI: “ng test” with Jest in 3 minutes - codeburst](https://codeburst.io/angular-6-ng-test-with-jest-in-3-minutes-b1fe5ed3417c)
* [【Jest】AngularでJestを使用する - 開発覚書はてな版](https://kakkoyakakko2.hatenablog.com/entry/2019/02/01/003000)
* [Use Jest instead of Karma and Jasmine for your Angular project](https://blog.upcoding.fr/use-jest-instead-of-karma-and-jasmin-for-your-angular-project/)
* Angular8
  * [File not found: jest-preset-angular/InlineHtmlStripStylesTransformer.js · Issue #332 · thymikee/jest-preset-angular](https://github.com/thymikee/jest-preset-angular/issues/332)  
  2019-11-04 現在、次の Issue でうまくいかない模様。  
  [Add Support for jest-preset-angular 8.0 · Issue #596 · just-jeb/angular-builders](https://github.com/just-jeb/angular-builders/issues/596)
  * [How I do configure Jest to test my Angular 8 Project - itnext](https://itnext.io/how-i-do-configure-jest-to-test-my-angular-8-project-2bd84a21d725)
* Nrwl Nx  
  Angular 8 に関しては、npm test で動かすことはできるものの ng generate などのコマンドが未対応で Angular CLI を捨てるのに似た状況。Nrwl Nxの拡張機能を使うほうが正道かもしれませんね。
  * [Nx 6.3: Faster Testing with Jest - Nrwl](https://blog.nrwl.io/nrwl-nx-6-3-faster-testing-with-jest-20a8ddb5064)
  * [Create Workspace with Angular defaults to Jest for testing · Issue #1698 · nrwl/nx](https://github.com/nrwl/nx/issues/1698)
  * [How to Set Up Angular Unit Testing with Jest - Amadou Sall](https://www.amadousall.com/how-to-set-up-angular-unit-testing-with-jest/)

## 余談

* Node.js を触り始めて日が浅いが、semantic versioning の major.minor.patch の minor あたりは割と破綻してるんじゃないかなと思う
  * 結局、依存VL合わせはうまくいかず、テストで動けばよいぐらい考えでよい気がしてきた  
    GitHub の `@angular-builders/jest` の package.json の dependency にある要求VLを見る限り、警告の出ない組み合わせは無理  
    → どうあるべきかなと考えて GNU autoconf を思い出した。
* Jest vs Karma
  * 対比してみて、Karma の知識が高まった。良くも悪くも、Karma の方がブラウザを使ったテストができる分、趣旨が異なる
  * Jest の方は、機能的なリグレッションテストに特化する使い方かな？  
    ブラウザ非互換とかの目的には使えない。スナップショットの管理は便利かなという印象。
  * Karma のサポートブラウザに JSDOM とかある。スピードだけで言えば、Karma も時間短縮なのかもしれない
  * Google と Facebook のアプローチがツールにも出ている気はする
     * Google(Karma) ... ブラウザやデバイス依存の機能など最新機能をどんどんリリースする
     * Facebook(Jest) ... あまり最新機能を追う必要のないから、ブラウザの互換のテストはそこまで頑張らなくてよい
* Angular8 は2019-11-04 現在、うまくいかなかった
  * 今、この瞬間に **Jestが超使いたい！**  ……というわけでもないので、別に良い
  * Jest押しの記事が増えているけれども、ちょっと一歩引いてみたほうが良さげな印象  
    もう少し、Karmaを使い込んでみて比較するのがいいか。