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を使う
直接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の指定やりたい。
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
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で起動。便利だ。
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 {} }
Material-UIのフォーム完成 & Firestoreへの登録
座標が取得できたときに、formikのsetValueで値を更新したらラベルと値が重なってしまう。 本当は初期値nullにしたかった
面数はSelectより type="number" の方がいいな...せっかく調べたが。 とりあえず入力画面こんな感じでできた。MVP。
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) } ... }