YSNHatenaBlog

主にアプリやWebサービス開発について

algoliasearchをモックする

algoliasearch がmockできない。

i  Running script: jest
 FAIL  src/functions/__tests__/index.test.ts
  ● Test suite failed to run

    TypeError: algoliasearch_1.default is not a function

      2 | import * as functions from 'firebase-functions'
      3 | 
    > 4 | const client = algoliasearch(
        |                             ^
      5 |   functions.config().algolia.app_id,
      6 |   functions.config().algolia.admin_key
      7 | )

      at Object.<anonymous> (src/functions/firestore/index.ts:4:29)
      at Object.<anonymous> (src/functions/__tests__/index.test.ts:13:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        6.62 s
Ran all test suites.

algoliaのclientはリクエストとレスポンスを上書きできるらしい。 https://discourse.algolia.com/t/mock-js-client-for-instantsearch-ui-intergration-testing/9751 https://www.algolia.com/doc/api-client/advanced/configure-the-client/javascript/?language=javascript&client=javascript#requester

これならいけそう。 https://stackoverflow.com/questions/47056694/jest-mocking-default-exports-require-vs-import/47058957 でもrequire使っていなはずなのだけど...。

jest.mock('algoliasearch', () => {
  return {
    default: jest.fn()
  }
})

jest.fn().mockReturnValue({})を事前に定義するとモックされない ↓ダメパターン

const mockSaveObject = jest.fn().mockReturnValue({})
jest.mock('algoliasearch', () => ({
  default: () => ({
    initIndex: () => ({
      saveObject: mockSaveObject,
    }),
  }),
}))

でもmockSaveObjectにアクセスできないと、この関数が呼ばれたかの判断ができない。

なにやらmockの初期化関数内でjest.fn()呼ばないとだめそう。 これならOK。

let mockSaveObject: ReturnType<typeof jest.fn>
jest.mock('algoliasearch', () => {
  mockSaveObject = jest.fn().mockReturnValue({})
  return {
    default: () => ({
      initIndex: () => ({
        saveObject: mockSaveObject,
      }),
    }),
  }
})

その他知ったこと

ややこしいリセット系
  • clearAllMocks - モックに格納された値だけリセット。
  • resetAllMocks - 上記に加えて実装もリセット。
  • restoreAllMocks - すべての振る舞いをオリジナルに戻す。ただしspyOnで作ったものだけ。
jestのオブジェクトの比較
toStrictEqual
jest中のconsoleログはsilentオプションで消せる
jest --silent

Cloud Functionsのテスト導入中

CloudFunctionsテスト導入。

このエラーが出る。

% yarn test
yarn run v1.16.0
$ firebase emulators:exec 'jest'
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
✔  functions: Using node@10 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: dist/public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/yosuke/Work/webapp/tennico/dist/functions" for Cloud Functions...
✔  functions[nextApp]: http function initialized (http://localhost:5001/tennico-f93a4/us-central1/nextApp).
✔  functions[courtCreated]: firestore function initialized.
✔  functions[courtUpdated]: firestore function initialized.
i  Running script: jest
 FAIL  src/functions/__tests__/index.test.ts
  ● Test suite failed to run

    src/functions/__tests__/index.test.ts:2:8 - error TS1259: Module '"/Users/yosuke/Work/webapp/tennico/node_modules/firebase-functions-test/lib/index"' can only be default-imported using the 'esModuleInterop' flag

    2 import functionsTest from 'firebase-functions-test'
             ~~~~~~~~~~~~~

      node_modules/firebase-functions-test/lib/index.d.ts:4:1
        4 export = _default;
          ~~~~~~~~~~~~~~~~~~
        This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        5.213 s

tsconfig見てない?試しに__test__直下にtsconfig置いて、compilerOptionsを親のものをコピペしてみる。

% yarn test
yarn run v1.16.0
$ firebase emulators:exec 'jest'
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
✔  functions: Using node@10 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: dist/public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/yosuke/Work/webapp/tennico/dist/functions" for Cloud Functions...
✔  functions[nextApp]: http function initialized (http://localhost:5001/tennico-f93a4/us-central1/nextApp).
✔  functions[courtCreated]: firestore function initialized.
✔  functions[courtUpdated]: firestore function initialized.
i  Running script: jest
 FAIL  src/functions/__tests__/index.test.ts
  ● Test suite failed to run

    Could not find a valid build in the '/Users/yosuke/Work/webapp/tennico/src/functions/next' directory! Try building your app with 'next build' before starting the server.

      4 | import * as url from 'url'
      5 | 
    > 6 | const app = next({
        |                 ^
      7 |   dev: false,
      8 |   dir: __dirname,
      9 |   conf: {

      at Server.readBuildId (node_modules/next/next-server/server/next-server.ts:1934:15)
      at new Server (node_modules/next/next-server/server/next-server.ts:192:25)
      at Function.createServer [as default] (node_modules/next/server/next.ts:41:10)
      at Object.<anonymous> (src/functions/index.ts:6:17)
      at Object.<anonymous> (src/functions/__tests__/index.test.ts:6:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.77 s
Ran all test suites.
(node:13284) ExperimentalWarning: The fs.promises API is experimental
⚠  Script exited unsuccessfully (code 1)
i  emulators: Shutting down emulators.
i  functions: Stopping Functions Emulator
i  hosting: Stopping Hosting Emulator
i  firestore: Stopping Firestore Emulator
i  hub: Stopping emulator hub

Error: Script "jest" exited with code 1
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

nextが呼ばれてしまうので、nextの設定がいろいろないとテストが実行されなそう。 functionsごとに別ファイルで切り出して、next以外の関数だけimportする。

更にハマる。またこれ。

FAIL  src/functions/__tests__/index.test.ts
  ● Test suite failed to run

    src/functions/__tests__/index.test.ts:2:8 - error TS1259: Module '"/Users/yosuke/Work/webapp/tennico/node_modules/firebase-functions-test/lib/index"' can only be default-imported using the 'esModuleInterop' flag

    2 import functionsTest from 'firebase-functions-test'
             ~~~~~~~~~~~~~

      node_modules/firebase-functions-test/lib/index.d.ts:4:1
        4 export = _default;
          ~~~~~~~~~~~~~~~~~~
        This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.

こうじゃなくて

import functionsTest from 'firebase-functions-test'

これでよかった

import * as functionsTest from 'firebase-functions-test'

前はlintエラーだったのに起きなくなった。

そしてこれ。え?まじか。

Cannot encode [object Object]to a Firestore Value. Local testing does not yet support Firestore geo points.

オブジェクトの構造だけ合わせる。

-          geo: new admin.firestore.GeoPoint(35.635557, 139.786987),
+          geo: {
+            _latitude: 35.635557,
+            _longitude: 139.786987,
+          },

あとはalgoliasearchをモックできれば動きそうな気配。

Firebaseのプロジェクトにテストを導入してみる(Jest入れるまで)

firebaseプロジェクトにテストを導入してみる。

馴染みがあるjestを使う。やり方は2種類。

直接Babelは使ってないのでts-jestで。型チェックも効く。

yarn add -D jest @types/jest ts-jest firebase-functions-test @firebase/rules-unit-testing

適当にテストを書くとLintエラー。import/exportがないモジュールは駄目。

index.test.ts cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.

こんな構成にしつつ、

src/functions
├── __tests__
│   ├── index.test.ts
│   └── tsconfig.json
├── index.ts
└── tsconfig.json

tests下のtsconfigはこうするとLintエラーが直る。

{
    "extends": "../tsconfig.json",
    "compilerOptions": {
      "isolatedModules": false
    },
}

jest.config.js

module.exports = {
  roots: ['<rootDir>/src'],
  testMatch: [
    '**/__tests__/**/*.+(ts|tsx|js)',
    '**/?(*.)+(spec|test).+(ts|tsx|js)',
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
}

package.jsonのscriptsに追加

+    "test": "jest"

動いた

% yarn test         
yarn run v1.16.0
$ jest
(node:9850) ExperimentalWarning: The fs.promises API is experimental
 PASS  src/functions/__tests__/index.test.ts
  ✓ basic (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.58 s, estimated 6 s
Ran all test suites.
✨  Done in 3.13s.

そのうちpathの指定やりたい。

qiita.com

emulatorの起動入れてみる。 package.json修正

"test": "firebase emulators:exec 'jest'"
% yarn test
yarn run v1.16.0
$ firebase emulators:exec 'jest'
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
✔  functions: Using node@10 from host.
i  firestore: downloading cloud-firestore-emulator-v1.11.11.jar...
Progress: ======================================================> (100% of 64MB
i  firestore: Removing outdated emulator files: cloud-firestore-emulator-v1.8.4.jar
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: dist/public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/yosuke/Work/webapp/tennico/dist/functions" for Cloud Functions...
⚠  It looks like you're trying to access functions.config().algolia but there is no value there. You can learn more about setting up config here: https://firebase.google.com/docs/functions/local-emulator
⚠  TypeError: Cannot read property 'app_id' of undefined
    at Object.<anonymous> (/Users/yosuke/Work/webapp/tennico/dist/functions/index.js:69:66)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Module.require (internal/modules/cjs/loader.js:690:17)
    at require (internal/modules/cjs/helpers.js:25:18)
    at initializeRuntime (/Users/yosuke/Work/webapp/tennico/node_modules/firebase-tools/lib/emulator/functionsEmulatorRuntime.js:676:29)
    at process._tickCallback (internal/process/next_tick.js:68:7)
⚠  We were unable to load your functions code. (see above)

構成変数が読めてないのでこれを実行。

firebase functions:config:get > .runtimeconfig.json

firebase.google.com

動いた。

参考

セキュリティルールのテスト firebase.google.com

Cloud Functionsのテスト firebase.google.com

Firestoreの設定をfirebase.jsonに入れる

そろそろテストを書いてみる。

firestoreのルールもテストしたいので、この際firestoreの設定も含めるようにしてみる。

index取得。

firebase firestore:indexes > firestore.indexes.json

ルールはコピペするしかない? firestore.rulesに書く。現状の内容をコピペ。一人しかwrite権限ナシ。

firebase.json編集

}
     ],
     "predeploy": "npm run build-public"
+  },
+  "firestore": {
+    "rules": "firestore.rules",
+    "indexes": "firestore.indexes.json"
   }
 }

reduxのデバッグ・リファクタリング

redux-dev-tools入れた

https://github.com/zalmoxisus/redux-devtools-extension

import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'
import { createFirestoreInstance, firestoreReducer } from 'redux-firestore'

const initialState = {}
const rootReducer = combineReducers({
  firebase: firebaseReducer,
  firestore: firestoreReducer,
})
const store = createStore(rootReducer, initialState, devToolsEnhancer({}))

Command + Shift + Eで起動。便利だ。

f:id:yosuke403:20210202055023p:plain

react-redux-firebaseのStoreに型がつくようになる。重要。 https://qiita.com/Takepepe/items/6addcb1b0facb8c6ff1f

import 'react-redux'

import { FirebaseReducer, FirestoreReducer } from 'react-redux-firebase'

export interface State {
  firebase: FirebaseReducer.Reducer<
    unknown,
    Record<string, Record<string | number, string | number>>
  >
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  firestore: FirestoreReducer.Reducer
}

declare module 'react-redux' {
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
  interface DefaultRootState extends State {}
}

編集機能を作る

編集画面作った。 新規登録のフォームを再利用する感じで。

f:id:yosuke403:20210201060656p:plain

フォームのコンポーネントが編集用の処理を含んでしまって複雑化している。 型の定義の仕方もあまりよくなかったかも。明日はリファクタだろうか。

Material-UIのフォーム完成 & Firestoreへの登録

座標が取得できたときに、formikのsetValueで値を更新したらラベルと値が重なってしまう。 本当は初期値nullにしたかった

面数はSelectより type="number" の方がいいな...せっかく調べたが。 とりあえず入力画面こんな感じでできた。MVP。

f:id:yosuke403:20210129061213p:plain

firestoreへの追加。関係あるところだけ。

import { useFirestore } from 'react-redux-firebase'

const NewCourt: React.FC<Record<string, unknown>> = () => {
  const firestore = useFirestore()

  const formik = useFormik({
    ...
    onSubmit: async (values, { setSubmitting }) => {
      const data: Omit<Court, 'id'> = {
        address: values.address,
        price: values.price,
        nighter: values.nighter,
        surfaces: {},
        name: values.name,
        createdAt: firebase.firestore.Timestamp.now(),
        geo: new firebase.firestore.GeoPoint(values.latitude, values.longitude),
        url: values.url,
      }
      try {
        const ref = await firestore.collection('courts').add(data)
        router.push(`/courts/${ref.id}`)
      } catch (e) {
        alert(e)
      }
      setSubmitting(false)
  }
  ...
}