├── .circleci └── config.yml ├── .eslintrc.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.yml ├── .storybook ├── main.cjs ├── package.json └── preview.ts ├── README.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── chapter1 │ ├── Greeter.test.ts │ └── Greeter.ts ├── chapter2 │ ├── assertion │ │ ├── assertion.test.ts │ │ ├── equalityOperators.js │ │ └── promise.ts │ ├── dns.ts │ ├── e2e │ │ ├── playwright.test.ts │ │ ├── puppeteer.test.ts │ │ └── selenium.test.ts │ ├── echo.ts │ ├── getting_started_jest │ │ ├── sum.test.ts │ │ └── sum.ts │ ├── group │ │ ├── calculateSalesTax.test.ts │ │ ├── concurrent.test.ts │ │ └── group.test.ts │ ├── hello.ts │ ├── mock │ │ ├── chohan.test.ts │ │ ├── chohan.ts │ │ ├── mock.test.ts │ │ ├── reset.test.ts │ │ ├── seed.ts │ │ ├── spyon.test.ts │ │ ├── users.test.ts │ │ └── users.ts │ ├── modules.ts │ └── ui │ │ ├── Button.stories.tsx │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ ├── __snapshots__ │ │ └── Button.test.tsx.snap │ │ ├── button.css │ │ ├── index.html │ │ └── ui.test.ts └── chapter3 │ ├── assertion.test.ts │ ├── unhandledReject.test.js │ ├── users.test.ts │ └── users.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | browser-tools: circleci/browser-tools@1.4.1 4 | 5 | jobs: # ジョブの設定 6 | test: 7 | docker: 8 | - image: cimg/node:18.14.0 9 | parallelism: 2 # 並列数を2で実行 10 | steps: 11 | - checkout # Githubからソースコードをチェックアウトする 12 | - restore_cache: 13 | keys: 14 | - node-v1-{{ checksum "package-lock.json" }} 15 | - run: 16 | name: Install node packages # 依存関係のインストール 17 | command: npm install 18 | - save_cache: 19 | paths: 20 | - ./node_modules 21 | key: node-v1-{{ checksum "package-lock.json" }} 22 | - run: 23 | name: Run lint and format # lint and format 24 | command: npm run lint && npm run format 25 | - run: 26 | name: Run Test # UIとE2E以外のテストを実行 27 | command: | 28 | npm test -- --shard=$(($CIRCLE_NODE_INDEX+1))/$CIRCLE_NODE_TOTAL src/chapter2/getting_started_jest src/chapter2/assertion src/chapter2/06_group src/chapter2/07_mock 29 | - store_test_results: # テスト結果をアップロード 30 | path: reports/jest 31 | - store_artifacts: # カバレッジをアップロード 32 | path: reports/coverage 33 | ui-test: 34 | docker: 35 | - image: cimg/node:18.14.0 36 | steps: 37 | - checkout 38 | - restore_cache: 39 | keys: 40 | - node-v2-{{ checksum "package-lock.json" }} 41 | - run: 42 | name: Install node packages 43 | command: npm install 44 | - save_cache: 45 | paths: 46 | - ./node_modules 47 | key: node-v2-{{ checksum "package-lock.json" }} 48 | - run: 49 | name: Run lint and format 50 | command: npm run lint && npm run format 51 | - run: 52 | name: Run UI Test 53 | command: npm test -- src/chapter2/ui 54 | - run: 55 | name: Upload Storybook to Chromatic 56 | command: npm run chromatic 57 | # 検証用のジョブになります。本書での説明はありません。 58 | e2e-test: 59 | docker: 60 | - image: cimg/node:18.14.0 61 | steps: 62 | - checkout 63 | - restore_cache: 64 | keys: 65 | - node-v2-{{ checksum "package-lock.json" }} 66 | - run: 67 | name: Install node packages 68 | command: npm install 69 | - save_cache: 70 | paths: 71 | - ./node_modules 72 | key: node-v2-{{ checksum "package-lock.json" }} 73 | - run: 74 | name: apt update 75 | command: sudo apt update 76 | - browser-tools/install-chrome: 77 | chrome-version: "110.0.5481.77" 78 | - browser-tools/install-firefox: 79 | version: "109.0.1" 80 | - run: 81 | name: Run E2E Test 82 | command: npm test -- src/chapter2/e2e/puppeteer.test.ts src/chapter2/e2e/selenium.test.ts 83 | # 検証用のジョブになります。本書での説明はありません。 84 | playwright-test: 85 | docker: 86 | - image: mcr.microsoft.com/playwright:v1.30.0-focal 87 | steps: 88 | - checkout 89 | - restore_cache: 90 | keys: 91 | - node-v2-{{ checksum "package-lock.json" }} 92 | - run: 93 | name: Install node packages 94 | command: npm install 95 | - save_cache: 96 | paths: 97 | - ./node_modules 98 | key: node-v2-{{ checksum "package-lock.json" }} 99 | - run: 100 | name: Run UI Test 101 | command: npm test -- src/chapter2/e2e/playwright.test.ts 102 | 103 | workflows: # ワークフローの設定 104 | test: 105 | jobs: 106 | - test 107 | - ui-test 108 | # - e2e-test 109 | - playwright-test 110 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - eslint:recommended 6 | - plugin:react/recommended 7 | - plugin:@typescript-eslint/recommended 8 | - prettier 9 | overrides: [] 10 | parser: '@typescript-eslint/parser' 11 | parserOptions: 12 | ecmaVersion: latest 13 | sourceType: module 14 | plugins: 15 | - react 16 | - '@typescript-eslint' 17 | rules: 18 | react/react-in-jsx-scope: 0 19 | settings: 20 | react: 21 | version: 18.2.0 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | storybook-static 4 | 5 | # E2E test recordings 6 | videos 7 | 8 | # jest-junit and jest coverage 9 | reports 10 | junit.xml 11 | 12 | build-storybook.log 13 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # すべてのファイルを除外する 2 | /**/*.* 3 | 4 | # src配下の.tsと.tsxのみ除外しない 5 | !src/**/*[.ts|.tsx] 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | semi: false 3 | singleQuote: true 4 | arrowParens: avoid 5 | -------------------------------------------------------------------------------- /.storybook/main.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions" 10 | ], 11 | "framework": "@storybook/react", 12 | "core": { 13 | "builder": "@storybook/builder-webpack5" 14 | }, 15 | features: { 16 | interactionsDebugger: true // interactionsDebuggerを有効化 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.storybook/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello-Jest-TypeScript 2 | 3 | 「[Jestではじめるテスト入門](https://peaks.cc/testing_with_jest)」の第1章から第4章で利用するサンプルコードです。 4 | 5 | ## パッケージのインストール 6 | ``` 7 | $ npm install 8 | ``` 9 | 10 | ## テストの実行 11 | ``` 12 | $ npm run test 13 | ``` 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest/presets/js-with-ts-esm', 3 | moduleNameMapper: { 4 | "\\.(css|less|scss)$": "identity-obj-proxy" 5 | }, 6 | reporters: [ 7 | "default", 8 | ['jest-junit', { outputDirectory: 'reports', outputName: 'jest-report.xml' }], 9 | ], 10 | collectCoverage: true, 11 | coverageReporters: ["text-summary", "html"], 12 | coverageDirectory: "reports/coverage", 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-jest-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "engines": { 8 | "node": "18.14.0", 9 | "npm": "9.3.1" 10 | }, 11 | "scripts": { 12 | "lint": "eslint src", 13 | "test": "jest", 14 | "prepare": "husky install", 15 | "format": "prettier --list-different src", 16 | "lint-staged": "lint-staged --allow-empty", 17 | "storybook": "start-storybook -p 6006", 18 | "build-storybook": "build-storybook", 19 | "chromatic": "chromatic --exit-once-uploaded --exit-zero-on-changes" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@babel/core": "7.20.12", 26 | "@storybook/addon-actions": "6.5.16", 27 | "@storybook/addon-essentials": "6.5.16", 28 | "@storybook/addon-interactions": "6.5.16", 29 | "@storybook/addon-links": "6.5.16", 30 | "@storybook/builder-webpack5": "6.5.16", 31 | "@storybook/manager-webpack5": "6.5.16", 32 | "@storybook/react": "6.5.16", 33 | "@storybook/testing-library": "0.0.13", 34 | "@testing-library/react": "^13.4.0", 35 | "@types/jest": "29.4.0", 36 | "@types/node": "18.14.2", 37 | "@types/puppeteer": "^7.0.4", 38 | "@types/react": "18.0.27", 39 | "@types/react-dom": "18.0.10", 40 | "@types/react-test-renderer": "18.0.0", 41 | "@types/selenium-webdriver": "4.1.10", 42 | "@typescript-eslint/eslint-plugin": "5.51.0", 43 | "@typescript-eslint/parser": "5.51.0", 44 | "axios": "1.3.4", 45 | "babel-loader": "8.3.0", 46 | "chromatic": "6.17.0", 47 | "chromedriver": "110.0.0", 48 | "eslint": "8.34.0", 49 | "eslint-config-prettier": "8.6.0", 50 | "eslint-plugin-react": "7.32.2", 51 | "geckodriver": "3.2.0", 52 | "husky": "8.0.3", 53 | "identity-obj-proxy": "3.0.0", 54 | "jest": "29.4.3", 55 | "jest-environment-jsdom": "29.4.3", 56 | "jest-junit": "15.0.0", 57 | "lint-staged": "13.1.1", 58 | "playwright": "1.30.0", 59 | "prettier": "2.8.4", 60 | "puppeteer": "19.6.3", 61 | "react": "18.2.0", 62 | "react-dom": "18.2.0", 63 | "react-test-renderer": "18.2.0", 64 | "selenium-webdriver": "4.8.0", 65 | "ts-jest": "29.0.5", 66 | "ts-node": "10.9.1", 67 | "typescript": "4.9.5" 68 | }, 69 | "lint-staged": { 70 | "*.{ts,tsx}": [ 71 | "eslint", 72 | "prettier --write" 73 | ] 74 | }, 75 | "readme": "ERROR: No README data found!", 76 | "_id": "hello-jest-ts@1.0.0" 77 | } 78 | -------------------------------------------------------------------------------- /src/chapter1/Greeter.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { Greeter } from './Greeter' 3 | 4 | describe('Greeter', () => { 5 | it.each([ 6 | ['Taka', 'Hello Taka'], 7 | ['Daniel', 'Hello Daniel'], 8 | ])('Says Hello and %s', (name, expected) => { 9 | const greeter = new Greeter() 10 | expect(greeter.greet(name)).toBe(expected) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/chapter1/Greeter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export class Greeter { 3 | greet(name: string) { 4 | return `Hello ${name}` 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/chapter2/assertion/assertion.test.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------- 2 | // testとitを利用したテストケースの作成 3 | // --------------------------------- 4 | 5 | test('testを利用してテストケースを作成する', () => { 6 | const result = true // テスト結果 7 | const expected = true // 期待値 8 | expect(result).toBe(expected) // expect関数とtoBe関数を利用して結果を評価する 9 | }) 10 | 11 | it('itを利用してテストケースを作成する', () => { 12 | expect(true).toBe(true) 13 | }) 14 | 15 | // --------------------------------- 16 | // プリミティブな値の評価 17 | // --------------------------------- 18 | 19 | const numberValue = 0 20 | const stringValue = '文字列' 21 | const booleanValue = true 22 | 23 | // toBeでプリミティブな値を評価 24 | test('evaluates as equal for all the same primitive values when using the toBe function', () => { 25 | expect(numberValue).toBe(0) 26 | expect(stringValue).toBe('文字列') 27 | expect(booleanValue).toBe(true) 28 | }) 29 | 30 | // toEqualでプリミティブな値を評価 31 | test('evaluates as equal for all the same primitive values when using the toEqual function', () => { 32 | expect(numberValue).toEqual(0) 33 | expect(stringValue).toEqual('文字列') 34 | expect(booleanValue).toEqual(true) 35 | }) 36 | 37 | // toStrictEqualでプリミティブな値を評価 38 | test('evaluates as equal for all the same primitive values when using the toStrictEqual function', () => { 39 | expect(numberValue).toStrictEqual(0) 40 | expect(stringValue).toStrictEqual('文字列') 41 | expect(booleanValue).toStrictEqual(true) 42 | }) 43 | 44 | // --------------------------------- 45 | // オブジェクトの評価 46 | // --------------------------------- 47 | 48 | // canの型を定義 49 | type CanType = { 50 | flavor: string 51 | ounces: number 52 | } 53 | 54 | // can1とcan2はそれぞれ同じプロパティと同じ値を持つ 55 | const can1: CanType = { 56 | flavor: 'grapefruit', 57 | ounces: 12, 58 | } 59 | 60 | const can2: CanType = { 61 | flavor: 'grapefruit', 62 | ounces: 12, 63 | } 64 | 65 | // can3はcan2の参照を持つ 66 | const can3: CanType = can2 67 | 68 | // Canクラス 69 | class Can { 70 | flavor: string 71 | ounces: number 72 | 73 | constructor({ flavor, ounces }: CanType) { 74 | this.flavor = flavor 75 | this.ounces = ounces 76 | } 77 | } 78 | 79 | // can4はCanクラスで生成されたオブジェクトでcan1、can2と同じプロパティを持つ 80 | const can4 = new Can({ 81 | flavor: 'grapefruit', 82 | ounces: 12, 83 | }) 84 | 85 | // --------------------------------- 86 | // toBe関数を利用したオブジェクトの評価 87 | // --------------------------------- 88 | 89 | // can1 と can2 は等しくないと評価される 90 | test('can1 and can2 are not the exact same instance', () => { 91 | expect(can1).not.toBe(can2) 92 | }) 93 | 94 | // can2 と can3 は等しいと評価される 95 | test('can2 and can3 are the same instance', () => { 96 | expect(can2).toBe(can3) 97 | }) 98 | 99 | // --------------------------------- 100 | // toEqual関数を利用したオブジェクトの評価 101 | // --------------------------------- 102 | 103 | // can1 と can2 は等しいと評価される 104 | test('can1 and can2 have the same properties', () => { 105 | expect(can1).toEqual(can2) 106 | }) 107 | 108 | // can2 と can4 は等しいと評価される 109 | test('can2 and can4 have the same properties', () => { 110 | expect(can2).toEqual(can4) 111 | }) 112 | 113 | // --------------------------------- 114 | // toStrictEqual関数を利用したオブジェクトの評価 115 | // --------------------------------- 116 | 117 | // can2 と can4 は等しくないと評価される 118 | test('can2 and can4 are defferent class', () => { 119 | expect(can2).not.toStrictEqual(can4) 120 | }) 121 | 122 | // 生成元クラスのチェック以外のtoEqualとtoStrictEqualの違い 123 | /* eslint-disable no-sparse-arrays */ 124 | test('differences between toEqual and toStrictEqual', () => { 125 | // toEqual:undefinedを持つプロパティが無視されるので、等しいと評価される 126 | expect({ foo: NaN, bar: undefined }).toEqual({ foo: NaN }) 127 | 128 | // toStrictEqual:undefinedを持つプロパティもチェックされるので、等しくないと評価される 129 | expect({ foo: NaN, bar: undefined }).not.toStrictEqual({ foo: NaN }) 130 | 131 | // toEqual:未定義の要素とundefinedの要素を区別しないので、等しいと評価される 132 | expect([, undefined, 1]).toEqual([undefined, , 1]) 133 | 134 | // toStrictEqual:未定義の要素とundefinedの要素を区別するので、等しくないと評価される 135 | expect([, undefined, 1]).not.toStrictEqual([undefined, , 1]) 136 | }) 137 | /* eslint-enable no-sparse-arrays */ 138 | 139 | // --------------------------------- 140 | // 曖昧な真偽値の評価 141 | // --------------------------------- 142 | 143 | test('"0" should be Truthy', () => { 144 | expect('0').toBeTruthy() 145 | }) 146 | 147 | test('0 should be Falsy', () => { 148 | expect(0).toBeFalsy() 149 | }) 150 | 151 | // --------------------------------- 152 | // null、undefinedの評価 153 | // --------------------------------- 154 | 155 | test('should be null', () => { 156 | expect(null).toBe(null) 157 | expect(null).toBeNull() 158 | }) 159 | 160 | test('should be undefined', () => { 161 | expect(undefined).toBe(undefined) 162 | expect(undefined).toBeUndefined() 163 | }) 164 | 165 | test('should be null or undefined', () => { 166 | // eslint-disable-next-line prefer-const 167 | let a // undefined 168 | expect(a == null).toBe(true) 169 | a = null // null 170 | expect(a == null).toBe(true) 171 | }) 172 | 173 | // --------------------------------- 174 | // 曖昧な結果の評価 175 | // --------------------------------- 176 | 177 | const hoge = () => ({ hoge: 'hogehoge', number: 0 }) 178 | 179 | test('hoge return anything', () => { 180 | // 期待値がnullやundefinedではないことを評価 181 | expect(hoge()).toEqual(expect.anything()) 182 | 183 | // 期待値の一部のプロパティがnullやundefinedではないことを評価 184 | expect(hoge()).toEqual({ 185 | hoge: 'hogehoge', 186 | number: expect.anything(), 187 | }) 188 | 189 | // 期待値の一部のプロパティnumberがNumber型であることを評価 190 | expect(hoge()).toEqual({ 191 | hoge: 'hogehoge', 192 | number: expect.any(Number), 193 | }) 194 | }) 195 | 196 | // --------------------------------- 197 | // 小数の計算で意図した結果にならない例 198 | // --------------------------------- 199 | 200 | // Number型がIEEE 754 倍精度浮動小数点数のため、小数点以下は2進数で計算されるため0.1 + 0.2 = 0.30000000000000004となる 201 | test('0.1 + 0.2 is not equal 0.3 due to IEEE 754 specification', () => { 202 | expect(0.1 + 0.2).not.toBe(0.3) 203 | }) 204 | 205 | // --------------------------------- 206 | // 浮動小数点数の誤差を許容した数値の評価 207 | // --------------------------------- 208 | 209 | test('0.1 + 0.2 returns 0.3', () => { 210 | expect(0.1 + 0.2).toBeCloseTo(0.3) // デフォルトでは小数点以下2桁までを評価する 211 | }) 212 | 213 | test('0.301 and 0.3 are different when numDigits is 3', () => { 214 | expect(0.3 + 0.001).not.toBeCloseTo(0.3, 3) // 小数点以下3桁目まで評価する場合、0.3と0.301は等しくないと評価する 215 | }) 216 | 217 | // --------------------------------- 218 | // 数値の比較 219 | // --------------------------------- 220 | 221 | // toBeGreaterThan 222 | test('0.1 + 0.2 is greater than 0.3', () => { 223 | expect(0.1 + 0.2).toBeGreaterThan(0.3) 224 | expect(0.1 + 0.2 > 0.3).toBe(true) 225 | }) 226 | // toBeGreaterThanOrEqual 227 | test('0.1 + 0.2 is greater than 0.3 or 0.1 + 0.2 equals to 0.30000000000000004', () => { 228 | expect(0.1 + 0.2).toBeGreaterThanOrEqual(0.3) 229 | expect(0.1 + 0.2).toBeGreaterThanOrEqual(0.30000000000000004) 230 | expect(0.1 + 0.2 >= 0.3).toBe(true) 231 | expect(0.1 + 0.2 >= 0.30000000000000004).toBe(true) 232 | }) 233 | // toBeLessThan 234 | test('0.1+0.2 is less than 0.4', () => { 235 | expect(0.1 + 0.2).toBeLessThan(0.4) 236 | expect(0.1 + 0.2 < 0.4).toBe(true) 237 | }) 238 | // toBeLessThanOrEqual 239 | test('0.1 + 0.2 is less than 0.4 or 0.1 + 0.2 equals to 0.30000000000000004', () => { 240 | expect(0.1 + 0.2).toBeLessThanOrEqual(0.4) 241 | expect(0.1 + 0.2).toBeLessThanOrEqual(0.30000000000000004) 242 | expect(0.1 + 0.2 <= 0.4).toBe(true) 243 | expect(0.1 + 0.2 <= 0.30000000000000004).toBe(true) 244 | }) 245 | 246 | // --------------------------------- 247 | // 文字列の部分一致(正規表現) 248 | // --------------------------------- 249 | 250 | const log1 = 251 | '10.0.0.3 - - [30/Jan/2023:12:20:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"' 252 | const log2 = 253 | '10.0.0.11 - - [30/Jan/2023:12:20:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"' 254 | const log3 = 255 | '10.0.0.99 - - [30/Jan/2023:12:20:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"' 256 | 257 | test('contains 10.0.0.3 IP address', () => { 258 | expect(log1).toEqual(expect.stringContaining('10.0.0.3')) 259 | }) 260 | 261 | test('contain IP address between 10.0.0.0 and 10.0.0.99', () => { 262 | // 10.0.0.0から10.0.0.99までのIPアドレスにマッチするための正規表現 263 | const expected = /^10.0.0.([1-9]?[0-9]) / 264 | 265 | // expect.stringMatching 266 | expect(log1).toEqual(expect.stringMatching(expected)) 267 | expect(log2).toEqual(expect.stringMatching(expected)) 268 | expect(log3).toEqual(expect.stringMatching(expected)) 269 | 270 | // toMatch 271 | expect(log1).toMatch(expected) 272 | expect(log2).toMatch(expected) 273 | expect(log3).toMatch(expected) 274 | 275 | // toBe 276 | const regex = new RegExp(expected) 277 | expect(regex.test(log1)).toBe(true) 278 | expect(regex.test(log2)).toBe(true) 279 | expect(regex.test(log3)).toBe(true) 280 | }) 281 | 282 | // --------------------------------- 283 | // 配列の部分一致 284 | // --------------------------------- 285 | 286 | // 配列の要素がプリミティブ型の場合 287 | const fruitList = ['Apple', 'Lemon', 'Orange'] 288 | 289 | // 1つの要素が含まれていることを検証 290 | test('contains Apple in fruitList', () => { 291 | expect(fruitList).toContain('Apple') 292 | }) 293 | 294 | // 複数の要素が含まれていることを検証 295 | test('contains Apple and Orange in fruitList', () => { 296 | expect(fruitList).toEqual(expect.arrayContaining(['Apple', 'Orange'])) 297 | }) 298 | 299 | // 配列の要素がオブジェクト型の場合 300 | const itemList = [ 301 | { name: 'Apple', price: 100 }, 302 | { name: 'Lemon', price: 150 }, 303 | { name: 'Orange', price: 120 }, 304 | ] 305 | 306 | // 1つの要素が含まれていることを検証 307 | test('contains Apple in itemList', () => { 308 | expect(itemList).toContainEqual({ name: 'Apple', price: 100 }) 309 | }) 310 | 311 | // 複数の要素が含まれていることを検証 312 | test('contains Apple and Orange in itemList', () => { 313 | expect(itemList).toEqual( 314 | expect.arrayContaining([ 315 | { name: 'Apple', price: 100 }, 316 | { name: 'Orange', price: 120 }, 317 | ]), 318 | ) 319 | }) 320 | 321 | // --------------------------------- 322 | // オブジェクトの部分一致 323 | // --------------------------------- 324 | 325 | const ciBuild = { 326 | number: 1, 327 | duration: 12000, 328 | state: 'success', 329 | triggerParameters: { 330 | is_scheduled: true, 331 | }, 332 | type: 'scheduled_pipeline', 333 | actor: { 334 | login: 'Taka', 335 | }, 336 | } 337 | 338 | // 1つのプロパティを検証 339 | test('build state should be success', () => { 340 | expect(ciBuild).toHaveProperty('state', 'success') 341 | }) 342 | 343 | // ネストしたプロパティを検証 344 | test('actor should be Taka', () => { 345 | expect(ciBuild).toHaveProperty('actor.login', 'Taka') 346 | }) 347 | 348 | // 複数のプロパティを検証 349 | test('triggered by the scheduled pipeline', () => { 350 | expect(ciBuild).toEqual( 351 | expect.objectContaining({ 352 | triggerParameters: expect.objectContaining({ is_scheduled: true }), 353 | type: 'scheduled_pipeline', 354 | }), 355 | ) 356 | }) 357 | 358 | // --------------------------------- 359 | // Errorの評価 360 | // --------------------------------- 361 | 362 | // Userクラスを定義 363 | class User { 364 | name: string 365 | password: string 366 | constructor({ name, password }: { name: string; password: string }) { 367 | // passwordが6文字未満の場合Errorをthrowする 368 | if (password.length < 6) 369 | throw new Error('The password length must be at least 6 characters.') 370 | this.name = name 371 | this.password = password 372 | } 373 | } 374 | 375 | // パスワードが6文字未満の場合にErrorがthrowされる 376 | test('creates a new user with a 6-character password', () => { 377 | expect(new User({ name: 'hoge', password: '123456' })).toEqual({ 378 | name: 'hoge', 379 | password: '123456', 380 | }) 381 | }) 382 | 383 | test('throw Error when the length of password is less than 6', () => { 384 | expect(() => new User({ name: 'hoge', password: '12345' })).toThrow() // Errorがthrowされたかのチェック 385 | expect(() => new User({ name: 'hoge', password: '12345' })).toThrow(Error) //型のチェック 386 | expect(() => new User({ name: 'hoge', password: '12345' })).toThrow( 387 | 'The password length must be at least 6 characters.', 388 | ) //エラーメッセージのチェック 389 | }) 390 | 391 | // --------------------------------- 392 | // Callback関数を利用した非同期な関数の結果の評価 393 | // --------------------------------- 394 | 395 | // (done関数を利用)コールバック関数の結果の評価 396 | const fetchDataWithCallback = callback => { 397 | setTimeout(callback, 3000, 'lemon') // 3秒経ってから`lemon`という文字列を返す 398 | } 399 | 400 | test('return lemon', done => { 401 | const callback = data => { 402 | expect(data).toBe('lemon') 403 | done() //テストの終了を宣言 404 | } 405 | fetchDataWithCallback(callback) 406 | }) 407 | 408 | // --------------------------------- 409 | // Promiseを利用した非同期な関数の結果の評価 410 | // --------------------------------- 411 | 412 | // .resolveを利用したPrmiose.resolveを返す関数の結果の評価 413 | const fetchDataWithPromiseResolve = () => 414 | new Promise(resolve => setTimeout(resolve, 1000, 'lemon')) 415 | 416 | // .resolvesを利用して成功時の値を受け取る 417 | test('return lemon', () => { 418 | return expect(fetchDataWithPromiseResolve()).resolves.toBe('lemon') 419 | }) 420 | 421 | // async/awaitを利用 422 | test('return lemon with async/await', async () => { 423 | await expect(fetchDataWithPromiseResolve()).resolves.toBe('lemon') 424 | }) 425 | 426 | // .rejects関数を利用した非同期な関数の例外処理の評価 427 | const fetchDataWithPromiseReject = () => 428 | new Promise((resolve, reject) => 429 | setTimeout(reject, 1000, new Error('lemon does not exist')), 430 | ) 431 | 432 | // .rejectsを利用して失敗時の値を受け取る 433 | test('failed to return lemon', () => { 434 | return expect(fetchDataWithPromiseReject()).rejects.toThrow( 435 | 'lemon does not exist', 436 | ) 437 | }) 438 | 439 | // async/awaitを利用 440 | test('failed to return lemon', async () => { 441 | await expect(fetchDataWithPromiseReject()).rejects.toThrow( 442 | 'lemon does not exist', 443 | ) 444 | }) 445 | -------------------------------------------------------------------------------- /src/chapter2/assertion/equalityOperators.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // 二重等号を利用した比較では暗黙的な型変換が発生する 4 | console.log('2' == 2) // true 5 | console.log(true == 1) // true 6 | 7 | // 三重等号を利用した比較では暗黙的な型変換は発生しない 8 | // 型が異なるため、等しくないと評価される 9 | console.log('2' === 2) // false 10 | console.log(true === 1) // false 11 | 12 | // Object.isはNaNと0を特別扱いしない 13 | // == 14 | console.log(NaN == NaN) // false 15 | console.log(+0 == -0) // true 16 | 17 | // === 18 | console.log(NaN == NaN) // false 19 | console.log(+0 == -0) // true 20 | 21 | // Object.is 22 | console.log(Object.is(NaN, NaN)) // true 23 | console.log(Object.is(+0, -0)) // false 24 | -------------------------------------------------------------------------------- /src/chapter2/assertion/promise.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars */ 2 | 3 | // --------------------------------- 4 | // そもそもPromiseとは? 5 | // --------------------------------- 6 | 7 | // プロミスの使い方 8 | const doSomethingAsync = () => { 9 | return new Promise((resolve, reject) => { 10 | // 非同期の処理が成功したときはresolve()を呼ぶ 11 | setTimeout(() => { 12 | resolve(true) 13 | }, 1000) 14 | // 非同期の処理が失敗したときにはreject()を呼ぶ 15 | // setTimeout(() => { reject(false) }, 1000) 16 | }) 17 | } 18 | 19 | const successCallback = () => { 20 | console.log('成功した') 21 | } 22 | const failureCallback = () => { 23 | console.log('失敗した') 24 | } 25 | 26 | // thenとcatchを利用した例 27 | doSomethingAsync().then(successCallback).catch(failureCallback) 28 | 29 | // thenのみを利用した例 30 | doSomethingAsync().then(successCallback, failureCallback) 31 | 32 | // コールバックでネストを繰り返した例 33 | const task = (callback, name, total) => { 34 | setTimeout(() => { 35 | total += 1 36 | console.log(`${name} finished! Total is ${total}.`) 37 | callback(total) 38 | }, 1000) 39 | } 40 | 41 | // prettier-ignore 42 | task(total => { 43 | task(total => { 44 | task(total => { 45 | task(total => { 46 | task(() => {},'task-5', total) 47 | },'task-4', total) 48 | },'task-3', total) 49 | },'task-2', total) 50 | },'task-1', 0) 51 | 52 | // コールバックをPromiseで書き直した例 53 | const taskPromise = (name, total) => { 54 | return new Promise(resolve => { 55 | setTimeout(() => { 56 | total += 1 57 | console.log(`${name} finished! Total is ${total}.`) 58 | resolve(total) 59 | }, 1000) 60 | }) 61 | } 62 | 63 | taskPromise('task-1', 0) 64 | .then(total => taskPromise('task-2', total)) 65 | .then(total => taskPromise('task-3', total)) 66 | .then(total => taskPromise('task-4', total)) 67 | .then(total => taskPromise('task-5', total)) 68 | -------------------------------------------------------------------------------- /src/chapter2/dns.ts: -------------------------------------------------------------------------------- 1 | import dns from 'node:dns' 2 | console.log(dns.getServers()) // DNSサーバーのIPアドレスを配列で返す 3 | -------------------------------------------------------------------------------- /src/chapter2/e2e/playwright.test.ts: -------------------------------------------------------------------------------- 1 | import { Browser, chromium, firefox, webkit } from 'playwright' 2 | 3 | describe.each([ 4 | { browserType: chromium, browserName: 'chromium' }, 5 | { browserType: firefox, browserName: 'firefox' }, 6 | { browserType: webkit, browserName: 'webkit' }, 7 | ])('e2e test with playwright and $browserName', ({ browserType }) => { 8 | let browser: Browser 9 | 10 | beforeAll(async () => { 11 | browser = await browserType.launch() // ブラウザの起動 12 | // 実際にブラウザの挙動を見たい場合はheadlessモードを無効化する 13 | // browser = await browserType.launch({ headless: false }) 14 | }) 15 | 16 | afterAll(async () => { 17 | await browser.close() //ブラウザの終了 18 | }) 19 | 20 | it('a search keyword will be on the page title in google.com', async () => { 21 | // 新しいページを立ち上げる 22 | const page = await browser.newPage() 23 | await page.goto('https://www.google.com/ncr') 24 | // 検索ボックスの要素を探し、playwrightを入力しエンターキーをクリック 25 | await page.type('input[name="q"]', 'playwright') 26 | await page.keyboard.press('Enter') 27 | 28 | // 次のページに切り替わるまで待つ 29 | await page.waitForURL('https://www.google.com/search?q=playwright*', { 30 | timeout: 2000, 31 | }) 32 | expect(await page.title()).toBe('playwright - Google Search') 33 | // ページを終了する 34 | await page.close() 35 | }) 36 | 37 | it('should work with Code generator test code', async () => { 38 | // 新しいページを立ち上げる 39 | const page = await browser.newPage() 40 | // Go to https://www.google.com/ コードジェネレーター上ではリダイレクトされ、`ncr`のパスが消えているため手動で追加 41 | await page.goto('https://www.google.com/ncr') 42 | // Click [aria-label="Search"] 43 | await page.locator('[aria-label="Search"]').click() 44 | // Fill [aria-label="Search"] 45 | await page.locator('[aria-label="Search"]').fill('playwright') 46 | // Press Enter 47 | await page.locator('[aria-label="Search"]').press('Enter') 48 | 49 | // コードジェネレーター上では次のURLをチェックしているが、検索毎にURLのパラメータは変わるため、ページタイトルを利用 50 | // 次のページに切り替わるまで待つ 51 | await page.waitForURL('https://www.google.com/search?q=playwright*', { 52 | timeout: 2000, 53 | }) 54 | expect(await page.title()).toBe('playwright - Google Search') 55 | // ページを終了する 56 | await page.close() 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/chapter2/e2e/puppeteer.test.ts: -------------------------------------------------------------------------------- 1 | import puppeteer, { Browser } from 'puppeteer' 2 | 3 | describe('e2e test with puppeteer', () => { 4 | let browser: Browser 5 | 6 | beforeAll(async () => { 7 | // 本書では解説していませんが、CI上で実行する際にはexecutablePathを設定する必要があります。 8 | if (process.env.CI) { 9 | browser = await puppeteer.launch({ 10 | executablePath: 'google-chrome-stable', 11 | }) 12 | } else { 13 | browser = await puppeteer.launch() 14 | } 15 | }) 16 | 17 | afterAll(async () => { 18 | await browser.close() 19 | }) 20 | 21 | it('a search keyword will be on the page title in google.com', async () => { 22 | // google.comにアクセス 23 | const page = await browser.newPage() 24 | await page.goto('https://www.google.com/ncr') 25 | 26 | // 検索ボックスの要素を探し、puppeteerを入力しエンターキーをクリック 27 | await page.type('input[name="q"]', 'puppeteer') 28 | await page.keyboard.press('Enter') 29 | 30 | // ページのタイトルが`puppeteer - Google Search`に切り替わるまで待つ 31 | await page.waitForNavigation({ 32 | timeout: 2000, 33 | waitUntil: 'domcontentloaded', 34 | }) 35 | expect(await page.title()).toBe('puppeteer - Google Search') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/chapter2/e2e/selenium.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebDriver, 3 | Builder, 4 | By, 5 | Key, 6 | until, 7 | Capabilities, 8 | } from 'selenium-webdriver' 9 | 10 | jest.setTimeout(20000) // タイムアウトを20秒に延長 11 | 12 | describe('e2e test with selenium and chromeDriver', () => { 13 | let chromeDriver: WebDriver 14 | 15 | beforeAll(async () => { 16 | const chromeCapabilities = Capabilities.chrome() 17 | chromeCapabilities.set('goog:chromeOptions', { 18 | args: [ 19 | '--headless', 20 | '--no-sandbox', 21 | '--disable-gpu', 22 | '--lang=en-US', 23 | // '--user-data-dir=./tmp_user_data', //--headlessを外す場合は有効化する 24 | ], 25 | }) 26 | 27 | // Chromeを起動しWebDriverのインスタンスを取得 28 | chromeDriver = await new Builder() 29 | .withCapabilities(chromeCapabilities) 30 | .build() 31 | }) 32 | 33 | afterAll(async () => { 34 | await chromeDriver.quit() // Chromeを停止する 35 | }) 36 | 37 | it('a search keyword will be on the page title in google.com', async () => { 38 | // google.comにアクセス 39 | await chromeDriver.get('https://www.google.com/ncr') 40 | // 検索ボックスの要素を探し、webdriver`、エンターキーを入力 41 | await chromeDriver 42 | .findElement(By.name('q')) 43 | .sendKeys('webdriver', Key.RETURN) 44 | // ページのタイトルが`webdriver - Google Search`であることを確認 45 | const results = await chromeDriver.wait( 46 | until.titleIs('webdriver - Google Search'), 47 | 10000, 48 | ) 49 | expect(results).toBe(true) 50 | }) 51 | }) 52 | 53 | describe('e2e test with selenium and geckoDriver', () => { 54 | let geckoDriver: WebDriver 55 | 56 | beforeAll(async () => { 57 | const fireFoxCapabilities = Capabilities.firefox() 58 | fireFoxCapabilities.set('moz:firefoxOptions', { 59 | args: ['-headless'], 60 | }) 61 | 62 | // Firefoxを起動しWebDriverのインスタンスを取得 63 | geckoDriver = await new Builder() 64 | .withCapabilities(fireFoxCapabilities) 65 | .build() 66 | }) 67 | 68 | afterAll(async () => { 69 | await geckoDriver.quit() 70 | }) 71 | 72 | it('a search keyword will be on the page title in google.com', async () => { 73 | // google.comにアクセス 74 | await geckoDriver.get('https://www.google.com/ncr') 75 | // 検索ボックスの要素を探し、webdriver`、エンターキーを入力 76 | await geckoDriver 77 | .findElement(By.name('q')) 78 | .sendKeys('webdriver', Key.RETURN) 79 | // ページのタイトルが`webdriver - Google Search`であることを確認 80 | const results = await geckoDriver.wait( 81 | until.titleIs('webdriver - Google Search'), 82 | 10000, 83 | ) 84 | expect(results).toBe(true) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/chapter2/echo.ts: -------------------------------------------------------------------------------- 1 | const echo = (message: string) => 2 | console.log(`The answer to life the universe and everything = ${message}`) 3 | echo('42') 4 | -------------------------------------------------------------------------------- /src/chapter2/getting_started_jest/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from './sum' 2 | 3 | // テストケースを定義 4 | test('1 + 2 equals 3', () => { 5 | // この例ではsum(1,2)を実行する際に結果として3が返されることを検証しています。 6 | expect(sum(1, 2)).toBe(3) 7 | }) 8 | -------------------------------------------------------------------------------- /src/chapter2/getting_started_jest/sum.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => a + b 2 | -------------------------------------------------------------------------------- /src/chapter2/group/calculateSalesTax.test.ts: -------------------------------------------------------------------------------- 1 | // 複数のテストケースを利用したcalculateSalesTax関数のテスト 2 | // 消費税を計算。税率を10%に固定。 3 | const calculateSalesTax = (price: number) => 4 | price > 0 ? Math.floor((price / 100) * 10) : 0 5 | 6 | describe('calculateSalesTax', () => { 7 | test('calculates the sales tax for a price equal to 100', () => { 8 | expect(calculateSalesTax(100)).toBe(10) 9 | }) 10 | 11 | test('calculates the sales tax for a price equal to 99', () => { 12 | expect(calculateSalesTax(99)).toBe(9) 13 | }) 14 | 15 | test('calculates the sales tax for a price equal to 1', () => { 16 | expect(calculateSalesTax(1)).toBe(0) 17 | }) 18 | 19 | test('calculates the sales tax for a price equal to 0.1', () => { 20 | expect(calculateSalesTax(0.1)).toBe(0) 21 | }) 22 | 23 | test('calculates the sales tax for a price equal to 0', () => { 24 | expect(calculateSalesTax(0)).toBe(0) 25 | }) 26 | 27 | test('calculates the sales tax for a price equal to -1', () => { 28 | expect(calculateSalesTax(-1)).toBe(0) 29 | }) 30 | }) 31 | 32 | // パラメタライズドテストを利用したcalculateSalesTax関数のテスト 33 | describe('calculateSalesTax with Parameterized Tests', () => { 34 | test.each([ 35 | { price: 100, expected: 10 }, 36 | { price: 99, expected: 9 }, 37 | { price: 1, expected: 0 }, 38 | { price: 0.1, expected: 0 }, 39 | { price: 0, expected: 0 }, 40 | { price: -1, expected: 0 }, 41 | ])( 42 | 'calculates the sales tax for a price equal to $price', 43 | ({ price, expected }) => { 44 | expect(calculateSalesTax(price)).toBe(expected) 45 | }, 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /src/chapter2/group/concurrent.test.ts: -------------------------------------------------------------------------------- 1 | // 1秒後に`lemon`文字列を返します 2 | const fetchData = () => 3 | new Promise(resolve => setTimeout(resolve, 1000, 'lemon')) 4 | 5 | // fetchData関数を100回実行するテスト 6 | // skipを追加 7 | test.concurrent.skip.each( 8 | Array.from(new Array(100).keys()).map(n => ({ 9 | n, 10 | expected: 'lemon', 11 | })), 12 | )('fetchData $n', async ({ n, expected }) => { 13 | console.log(n) 14 | await expect(fetchData()).resolves.toBe(expected) 15 | }) 16 | -------------------------------------------------------------------------------- /src/chapter2/group/group.test.ts: -------------------------------------------------------------------------------- 1 | describe('グループ名', () => { 2 | test('テストケース1', () => { 3 | expect(true).toBe(true) 4 | }) 5 | test('テストケース2', () => { 6 | expect(true).toBe(true) 7 | }) 8 | test('テストケース3', () => { 9 | expect(true).toBe(true) 10 | }) 11 | 12 | // 入れ子でグループを定義できる 13 | describe('グループ名', () => { 14 | test('テストケース', () => { 15 | expect(true).toBe(true) 16 | }) 17 | }) 18 | }) 19 | 20 | // グループ1 21 | describe('before/after timing', () => { 22 | // グループ1の前後処理 23 | beforeAll(() => console.log('1 - beforeAll')) 24 | afterAll(() => console.log('1 - afterAll')) 25 | beforeEach(() => console.log('1 - beforeEach')) 26 | afterEach(() => console.log('1 - afterEach')) 27 | // グループ1のテスト1 28 | test('', () => console.log('1 - test1')) 29 | // グループ2 30 | describe('Scoped / Nested block', () => { 31 | // グループ2の前後処理 32 | beforeAll(() => console.log('2 - beforeAll')) 33 | afterAll(() => console.log('2 - afterAll')) 34 | beforeEach(() => console.log('2 - beforeEach')) 35 | afterEach(() => console.log('2 - afterEach')) 36 | // グループ2のテスト1 37 | test('', () => console.log('2 - test1')) 38 | // グループ2のテスト2 39 | test('', () => console.log('2 - test2')) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/chapter2/hello.ts: -------------------------------------------------------------------------------- 1 | console.log('Hello TypeScript') 2 | -------------------------------------------------------------------------------- /src/chapter2/mock/chohan.test.ts: -------------------------------------------------------------------------------- 1 | import { chohan } from './chohan' 2 | 3 | jest.mock('./seed', () => { 4 | // seedをモック化する 5 | return { 6 | seed: jest 7 | .fn() 8 | .mockImplementationOnce(() => 2) // 1回目に偶数を返す 9 | .mockImplementationOnce(() => 1), // 2回目に奇数を返す 10 | } 11 | }) 12 | 13 | describe('chohan', () => { 14 | it('returns 丁 when seed returns an even number like 2', () => { 15 | expect(chohan()).toBe('丁') 16 | }) 17 | it('returns 半 when seed returns an odd number like 1', () => { 18 | expect(chohan()).toBe('半') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/chapter2/mock/chohan.ts: -------------------------------------------------------------------------------- 1 | import { seed } from './seed' 2 | 3 | // 丁か半を返す、内部的にseed関数を呼び出す 4 | export const chohan = () => (seed() % 2 === 0 ? '丁' : '半') 5 | -------------------------------------------------------------------------------- /src/chapter2/mock/mock.test.ts: -------------------------------------------------------------------------------- 1 | describe('jest.fn()', () => { 2 | test('mock object specification', () => { 3 | const mockFunction = jest.fn() 4 | 5 | // mockFunction関数の結果は`undefined`である 6 | expect(mockFunction('foo', 'bar')).toBe(undefined) 7 | 8 | // mockプロパティを持っている 9 | expect(mockFunction).toHaveProperty('mock') 10 | 11 | // mockにはcallsプロパティを持っている 12 | expect(mockFunction.mock).toHaveProperty('calls') 13 | 14 | // 1回呼び出された 15 | expect(mockFunction.mock.calls).toHaveLength(1) 16 | 17 | // 1回呼び出された際に、引数は'foo'と'bar'だった 18 | expect(mockFunction.mock.calls[0]).toEqual(['foo', 'bar']) 19 | 20 | // mockにはresultsプロパティを持っている 21 | expect(mockFunction.mock).toHaveProperty('results') 22 | 23 | // 1回呼び出された 24 | expect(mockFunction.mock.results).toHaveLength(1) 25 | 26 | // 1回目の返り値はundefinedである 27 | expect(mockFunction.mock.results[0].value).toBe(undefined) 28 | 29 | // 1回目の呼び出しが正常終了した 30 | expect(mockFunction.mock.results[0].type).toBe('return') 31 | }) 32 | }) 33 | 34 | // mockImplementationで返り値を設定 35 | test('return `Hello`', () => { 36 | const mockFunction = jest.fn(() => 'Hello') 37 | // const mockFunction = jest.fn().mockImplementation(() => 'Hello') // 上記と同じ設定 38 | expect(mockFunction()).toBe('Hello') 39 | }) 40 | 41 | // mockImplementationOnceで呼び出し毎に異なる返り値を設定 42 | test('return `Hello` once then it returns `Goodbye`', () => { 43 | const mockFunction = jest 44 | .fn() 45 | .mockImplementationOnce(() => 'Hello') 46 | .mockImplementationOnce(() => 'Goodbye') 47 | 48 | expect(mockFunction()).toBe('Hello') 49 | expect(mockFunction()).toBe('Goodbye') 50 | expect(mockFunction()).toBe(undefined) // デフォルトの返り値である`undefined`がリターンされる 51 | }) 52 | -------------------------------------------------------------------------------- /src/chapter2/mock/reset.test.ts: -------------------------------------------------------------------------------- 1 | describe('#reset mocks with jest.fn', () => { 2 | const targetDate = '2020-12-25' 3 | const mockDate = new Date('2019-12-25') //targetDateの1年前 4 | 5 | beforeEach(() => { 6 | // eslint-disable-next-line no-global-assign 7 | Date = jest.fn(() => mockDate) as unknown as jest.MockedFunction< 8 | typeof Date 9 | > 10 | }) 11 | 12 | it('jest.clearAllMocks', () => { 13 | // new DateでmockDate以外の値を指定してもモック化されているため、必ずmockDateがリターンされる 14 | expect(new Date(targetDate)).toEqual(mockDate) 15 | // new Dateの引数であるtargetDateの値がセットされている 16 | expect((Date as jest.MockedFunction).mock.calls).toEqual([ 17 | ['2020-12-25'], 18 | ]) 19 | expect((Date as jest.MockedFunction).mock.results).toEqual([ 20 | { type: 'return', value: mockDate }, 21 | ]) 22 | 23 | // リセット 24 | jest.clearAllMocks() 25 | 26 | // mockのプロパティがすべてリセットされる 27 | expect((Date as jest.MockedFunction).mock.calls).toEqual([]) 28 | expect((Date as jest.MockedFunction).mock.results).toEqual([]) 29 | 30 | // mock関数は引き続き利用できる 31 | expect(new Date(targetDate)).toEqual(mockDate) 32 | }) 33 | 34 | it('jest.resetAllMocks', () => { 35 | expect(new Date(targetDate)).toEqual(mockDate) 36 | expect((Date as jest.MockedFunction).mock.calls).toEqual([ 37 | ['2020-12-25'], 38 | ]) 39 | expect((Date as jest.MockedFunction).mock.results).toEqual([ 40 | { type: 'return', value: mockDate }, 41 | ]) 42 | 43 | // リセット 44 | jest.resetAllMocks() 45 | 46 | // mockのプロパティがすべてリセットされる 47 | expect((Date as jest.MockedFunction).mock.calls).toEqual([]) 48 | expect((Date as jest.MockedFunction).mock.results).toEqual([]) 49 | 50 | // mock関数もリセットされ、デフォルトでは`{}`が返される 51 | expect(new Date(targetDate)).toEqual({}) 52 | }) 53 | 54 | it('jest.restoreAllMocks', () => { 55 | expect(new Date(targetDate)).toEqual(mockDate) 56 | expect((Date as jest.MockedFunction).mock.calls).toEqual([ 57 | ['2020-12-25'], 58 | ]) 59 | expect((Date as jest.MockedFunction).mock.results).toEqual([ 60 | { type: 'return', value: mockDate }, 61 | ]) 62 | 63 | // リセット 64 | jest.restoreAllMocks() 65 | 66 | // mockのプロパティがすべてリセットされる 67 | expect((Date as jest.MockedFunction).mock.calls).toEqual([]) 68 | expect((Date as jest.MockedFunction).mock.results).toEqual([]) 69 | 70 | // mock関数もリセットされ、デフォルトでは`{}`が返される 71 | // この例は、jest.fn()を利用したが、jest.spyOn()でモック化した場合はオリジナルの関数へ戻る 72 | expect(new Date(targetDate)).toEqual({}) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/chapter2/mock/seed.ts: -------------------------------------------------------------------------------- 1 | // 0から9をランダムで返す 2 | export const seed = () => Math.floor(Math.random() * 10) 3 | -------------------------------------------------------------------------------- /src/chapter2/mock/spyon.test.ts: -------------------------------------------------------------------------------- 1 | describe('Math.random with spyOn', () => { 2 | let spy 3 | 4 | afterEach(() => { 5 | spy.mockRestore() // モック関数を元の関数へ戻す 6 | // jest.restoreAllMocks() // すべてのモック化した関数をオリジナルの関数へ戻す 7 | }) 8 | 9 | it('Math.random return 1', () => { 10 | spy = jest.spyOn(Math, 'random').mockImplementation(() => 1) // Math.random()は1を返す。元の関数では0から1未満を返す。 11 | expect(Math.random()).toBe(1) 12 | }) 13 | 14 | it('Math.random return under 1', () => { 15 | expect(Math.random()).toBeLessThan(1) // 1未満である 16 | // expect(Math.random() < 1).toBe(true) // 上記のtoBeLessThanをtoBeで評価した例 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/chapter2/mock/users.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import Users from './users' 3 | 4 | jest.mock('axios') 5 | 6 | test('should fetch all users', async () => { 7 | const users = [{ name: 'Bob' }] 8 | const resp = { data: users } 9 | 10 | ;(axios as jest.Mocked).get.mockResolvedValue(resp) 11 | // axios.get.mockImplementation(() => Promise.resolve(resp)) // 上記のmockResolvedValueと同じ設定 12 | 13 | await expect(Users.search()).resolves.toEqual(users) 14 | }) 15 | -------------------------------------------------------------------------------- /src/chapter2/mock/users.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default class Users { 4 | static search() { 5 | return axios.get('/users').then(resp => resp.data) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/chapter2/modules.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // ESM 3 | import dns from 'node:dns' 4 | export const foo = () => console.log(dns.getServers()) 5 | 6 | // CommonJS 7 | const _dns = require('node:dns') 8 | const bar = () => console.log(_dns.getServers()) 9 | module.exports = bar 10 | -------------------------------------------------------------------------------- /src/chapter2/ui/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react' 2 | import { screen, userEvent } from '@storybook/testing-library' // @storybook/testing-libraryを追加 3 | import { Button } from './Button' 4 | 5 | export default { component: Button } as ComponentMeta 6 | export const Primary: ComponentStoryObj = {} 7 | export const Secondary: ComponentStoryObj = { 8 | args: { primary: false }, 9 | } 10 | 11 | // ボタンをクリックするストーリー 12 | export const ClickButton: ComponentStoryObj = { 13 | play: () => { 14 | const button = screen.getByRole('button') 15 | userEvent.click(button) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/chapter2/ui/Button.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import renderer from 'react-test-renderer' 6 | import { render, fireEvent } from '@testing-library/react' 7 | import { Button } from './Button' 8 | 9 | describe('Button', () => { 10 | // react-test-rendererを利用した、スナップショットテスト 11 | it('renders correctly with react-test-renderer', () => { 12 | const button = renderer.create( 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/chapter2/ui/__snapshots__/Button.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button renders correctly with react-test-renderer 1`] = ` 4 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/chapter2/ui/button.css: -------------------------------------------------------------------------------- 1 | button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | font-size: 14px; 10 | padding: 11px 20px; 11 | } 12 | 13 | .primary { 14 | color: white; 15 | background-color: #1ea7fd; 16 | } 17 | 18 | .secondary { 19 | color: #333; 20 | background-color: transparent; 21 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 22 | } 23 | -------------------------------------------------------------------------------- /src/chapter2/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello Jest 5 | 6 | 7 | 8 | 9 |
10 |

Hello Jest

11 |
12 | 13 |
14 | 15 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/chapter2/ui/ui.test.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM, DOMWindow } from 'jsdom' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const html = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf8') 6 | 7 | describe('simple ui test', () => { 8 | let document: Document 9 | let window: DOMWindow 10 | 11 | beforeEach(() => { 12 | window = new JSDOM(html, { runScripts: 'dangerously' }).window 13 | document = window.document 14 | }) 15 | 16 | // ボタンがクリックされていない場合に、「message」が表示されていないこと 17 | it("doesn't show a message at the initial state", () => { 18 | const message = document.querySelector('#message > p') //message配下のpタグ要素を取得 19 | expect(message).toBe(null) 20 | }) 21 | 22 | // ボタンがクリックされたら、「You Passed!!!」が表示されること 23 | it('shows a message after clicking the button', () => { 24 | const button = document.querySelector('#showMessage') // showMessageボタンの要素を取得 25 | const click = new window.MouseEvent('click') 26 | button?.dispatchEvent(click) // buttonをクリックする 27 | 28 | const message = document.querySelector('#message > p') //message配下のpタグ要素を取得 29 | expect(message?.textContent).toBe('You Passed!!!') 30 | }) 31 | 32 | // ボタンが2回クリックされても、「You Passed!!!」が1つしか表示されないこと 33 | it('shows only one message after clicking the button twice', () => { 34 | const button = document.querySelector('#showMessage') 35 | const click = new window.MouseEvent('click') 36 | button?.dispatchEvent(click) 37 | button?.dispatchEvent(click) // 2回ボタンをクリックする 38 | 39 | const messages = document.querySelectorAll('#message > p') 40 | expect(messages.length).toBe(1) // 要素が1つであること 41 | expect(messages[0].textContent).toBe('You Passed!!!') // テキストに`You Passed!!!`が含まれること 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/chapter3/assertion.test.ts: -------------------------------------------------------------------------------- 1 | // foo関数 2 | const foo = () => ({ 3 | bar: { 4 | status: 'apply', 5 | }, 6 | }) 7 | 8 | // 改善前 9 | // test('foo', () => { 10 | // const result = foo() 11 | 12 | // // 1つのオブジェクトに対して複数のチェックを行っている 13 | // expect(result).toHaveProperty('bar') 14 | // expect(result['bar']).toHaveProperty('status') 15 | // expect(result['bar'].status).toBe('apply') 16 | // }) 17 | 18 | // 改善後 19 | test('foo', () => { 20 | const result = foo() 21 | 22 | expect(result).toEqual({ 23 | bar: { 24 | status: 'apply', 25 | }, 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/chapter3/unhandledReject.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fetchDataReject = () => 3 | new Promise((resolve, reject) => 4 | setTimeout(() => reject(new Error('Failed fetching data')), 1000, {}), 5 | ) 6 | 7 | // パブリックな関数と想定 8 | const magicalFunction = () => { 9 | const response = fetchDataReject() 10 | return response ? true : false 11 | } 12 | 13 | // Promiseがキャッチできないため、テストケースは成功となるが、コンソールにはエラーが表示される 14 | // 実際に試す場合は test.skip の .skipを削除 15 | test.skip('magicalFunction returns true if fetchDataReject returns any object', () => { 16 | expect(magicalFunction()).toBe(true) 17 | }) 18 | -------------------------------------------------------------------------------- /src/chapter3/users.test.ts: -------------------------------------------------------------------------------- 1 | import * as users from './users' 2 | 3 | describe('getNameList', () => { 4 | test('すべてのユーザー名を返す', async () => { 5 | const expected = ['Bob'] 6 | 7 | // getUsersをモック化 8 | jest 9 | .spyOn(users, 'getUsers') 10 | .mockReturnValueOnce(Promise.resolve([{ name: 'Bob' }])) 11 | 12 | await expect(users.getNameList()).resolves.toEqual(expected) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/chapter3/users.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const getNameList = async () => { 4 | const users = await getUsers() 5 | return users.map(user => user.name) 6 | } 7 | 8 | export const getUsers = async () => { 9 | const users = await axios.get('/users').then(resp => resp.data) 10 | 11 | // APIのデータから限られた項目のみ抽出している 12 | return users.map(user => ({ 13 | name: `${user.lastName} ${user.firstName}`, 14 | age: user.age, 15 | isDeleted: user.isDeleted, 16 | })) 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | "jsx": "react-jsx", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | /* Modules */ 24 | "module": "esnext", /* Specify what module code is generated. */ 25 | // "rootDir": "./", /* Specify the root folder within your source files. */ 26 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 30 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 31 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 32 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 33 | // "resolveJsonModule": true, /* Enable importing .json files */ 34 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 35 | /* JavaScript Support */ 36 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 39 | /* Emit */ 40 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 41 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 42 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 43 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 44 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 45 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 46 | // "removeComments": true, /* Disable emitting comments. */ 47 | // "noEmit": true, /* Disable emitting files from a compilation. */ 48 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 49 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 50 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 51 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 54 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 55 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 56 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 57 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 58 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 59 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 60 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 61 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 62 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 63 | /* Interop Constraints */ 64 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 66 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 67 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 68 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 69 | /* Type Checking */ 70 | "strict": true, /* Enable all strict type-checking options. */ 71 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 72 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 73 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 74 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 75 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 76 | "noImplicitThis": false, /* Enable error reporting when `this` is given the type `any`. */ 77 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 78 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 79 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 80 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 81 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 82 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 83 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 84 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 85 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 86 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 87 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 88 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 89 | /* Completeness */ 90 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 91 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 92 | }, 93 | "include": [ 94 | "./src/**/*.ts", 95 | "./src/**/*.tsx" 96 | ], 97 | "ts-node": { 98 | "esm": true, 99 | }, 100 | } 101 | --------------------------------------------------------------------------------