【问题标题】:How do you mock Firebase Firestore methods using Jest?您如何使用 Jest 模拟 Firebase Firestore 方法?
【发布时间】:2019-02-02 06:17:06
【问题描述】:

我有一系列函数,每个函数都执行各种 firestore 交互。如何使用 Jest 模拟这些 Firestore 调用?我想避免使用库。

当我使用 jest.mock("firebase/app")jest.mock("firebase/firestore") 以及其他变体时,我得到 null TypeErrors,或者错误表明我仍在引用实际导入而不是模拟:Error: ... make sure you call initializeApp()

例如我要测试的一个简单函数:

import firebase from "firebase/app";
import "firebase/firestore";

export const setDocData = (id, data) => {
  const newDoc = {
    created: firebase.firestore.FieldValue.serverTimestamp(),
    ...data
  };
  firebase
    .firestore()
    .doc("docs/" + id)
    .set(newDoc);
};

注意如何像往常一样导入 firebase,然后导入 firestore 会产生副作用。另请注意,firestore 如何首先作为函数调用,然后再作为属性引用。我相信这是我麻烦的根源。

【问题讨论】:

    标签: javascript firebase google-cloud-firestore jestjs


    【解决方案1】:

    这是我找到的解决方案。网上资料不多,希望对大家有所帮助。

    编辑:我相信你可以使用 jests /__MOCKS__/ 做类似的事情 文件夹系统,而不是像我在这个 例子。

    诀窍是创建模拟函数的链式 API,并将其设置在 firebase 对象上,而不是导入和模拟 firestore。下面的例子让我可以测试上面的示例函数,还有doc().get() promises。

    const docData = { data: "MOCK_DATA" };
    const docResult = {
      // simulate firestore get doc.data() function
      data: () => docData
    };
    const get = jest.fn(() => Promise.resolve(docResult));
    const set = jest.fn();
    const doc = jest.fn(() => {
      return {
        set,
        get
      };
    });
    const firestore = () => {
      return { doc };
    };
    firestore.FieldValue = {
      serverTimestamp: () => {
        return "MOCK_TIME";
      }
    };
    
    export { firestore };
    

    我在所有测试之前运行的文件中声明它(请参阅文档),然后在我的测试文件中导入并使用它,如下所示:

    import firebase from "firebase/app";
    import { firestore } from "../setupTests";
    firebase.firestore = firestore;
    
    describe("setDocData", () => {
      const mockData = { fake: "data" };
      beforeEach(() => {
        jest.clearAllMocks();
        setDocData("fakeDocID", mockData);
      });
    
      it("writes the correct doc", () => {
        expect(firestore().doc).toHaveBeenCalledWith("docs/fakeDocID");
      });
    
      it("adds a timestamp, and writes it to the doc", () => {
        expect(firestore().doc().set).toHaveBeenCalledWith({
          created: "MOCK_TIME",
          fake: "data"
        });
      });
    });
    

    【讨论】:

      【解决方案2】:

      这就是我开玩笑地嘲笑 firebase 的方式。

      'use strict'
      
      const collection = jest.fn(() => {
        return {
          doc: jest.fn(() => {
            return {
              collection: collection,
              update: jest.fn(() => Promise.resolve(true)),
              onSnapshot: jest.fn(() => Promise.resolve(true)),
              get: jest.fn(() => Promise.resolve(true))
            }
          }),
          where: jest.fn(() => {
            return {
              get: jest.fn(() => Promise.resolve(true)),
              onSnapshot: jest.fn(() => Promise.resolve(true)),
            }
          })
        }
      });
      
      const Firestore = () => {
        return {
          collection
        }
      }
      
      Firestore.FieldValue = {
        serverTimestamp: jest.fn()
      }
      
      export default class RNFirebase {
      
        static initializeApp = jest.fn();
      
        static auth = jest.fn(() => {
          return {
            createUserAndRetrieveDataWithEmailAndPassword: jest.fn(() => Promise.resolve(true)),
            sendPasswordResetEmail: jest.fn(() => Promise.resolve(true)),
            signInAndRetrieveDataWithEmailAndPassword: jest.fn(() => Promise.resolve(true)),
            fetchSignInMethodsForEmail: jest.fn(() => Promise.resolve(true)),
            signOut: jest.fn(() => Promise.resolve(true)),
            onAuthStateChanged: jest.fn(),
            currentUser: {
              sendEmailVerification: jest.fn(() => Promise.resolve(true))
            }
          }
        });
      
        static firestore = Firestore;
      
        static notifications = jest.fn(() => {
          return {
              onNotification: jest.fn(),
              onNotificationDisplayed: jest.fn(),
              onNotificationOpened: jest.fn()
          }
        });
      
        static messaging = jest.fn(() => {
          return {
              hasPermission: jest.fn(() => Promise.resolve(true)),
              subscribeToTopic: jest.fn(),
              unsubscribeFromTopic: jest.fn(),
              requestPermission: jest.fn(() => Promise.resolve(true)),
              getToken: jest.fn(() => Promise.resolve('RN-Firebase-Token'))
          }
        });
      
        static storage = jest.fn(() => {
          return {
            ref: jest.fn(() => {
              return {
                child: jest.fn(() => {
                  return {
                    put: jest.fn(() => Promise.resolve(true))
                  }
                })
              }
            })
          }
        })
      
      }
      

      【讨论】:

      • 谢谢,我如何检查例如在 Firestore 集合中检索到的特定 where 条件
      • 类似expect(collection.where).toHaveBeenCalledWith('assignedNumbers.123', '==', true);我的意思是
      【解决方案3】:

      我在组件上使用了依赖注入方法,这意味着我可以在没有所有样板的情况下模拟和测试方法。

      例如,我有一个处理邀请的表单组件,如下所示:

      import React, { useEffect } from 'react';
      import { Formik } from 'formik';
      import { validations } from '../../helpers';
      import { checkIfTeamExists } from '../helpers';
      
      const Invite = ({ send, userEmail, handleTeamCreation, auth, db, dbWhere }) => {
        useEffect(() => {
          checkIfTeamExists(send, dbWhere);
        }, []);
        return (
            <Formik
              initialValues={{ email: '' }}
              onSubmit={values =>
                handleTeamCreation(userEmail, values.email, db, auth, send)
              }
              validate={validations}
              render={props => (
                <form onSubmit={props.handleSubmit} data-testid="form">
                  <input
                    type="email"
                    placeholder="Please enter your email."
                    onChange={props.handleChange}
                    onBlur={props.handleBlur}
                    value={props.values.email}
                    name="email"
                  />
                  {props.errors.email && (
                    <p className="red" data-testid="error">
                      {props.errors.email}
                    </p>
                  )}
                  <button type="submit">Submit</button>
                </form>
              )}
            />
        );
      };
      
      export default Invite;
      

      checkIfTeamExists 方法依赖于 firebase 身份验证,handleTeamCreation 方法写入 firestore。

      当我在其父组件中引用该组件时,我像这样实例化它:

      <Invite
       send={send}
       userEmail={value.user.user.email}
       handleTeamCreation={handleTeamCreation}
       auth={auth.sendSignInLinkToEmail}
       db={db.collection('games')}
       dbWhere={db.collection('games')
                  .where('player1', '==', value.user.user.email)
                  .get}
       />
      

      然后,使用react-testing-library,在我的测试中,我能够用一个简单的jest.fn() 模拟出事情。

      test('Invite form fires the send function on Submit ', async () => {
        const handleTeamCreation = jest.fn();
        const send = jest.fn();
        const userEmail = 'ex@mple.com';
        const db = jest.fn();
        const auth = jest.fn();
        const dbWhere = jest.fn().mockResolvedValue([]);
        const { getByPlaceholderText, getByTestId } = render(
          <Invite
            send={send}
            userEmail={userEmail}
            handleTeamCreation={handleTeamCreation}
            auth={auth}
            db={db}
            dbWhere={dbWhere}
          />
        );
        const inputNode = getByPlaceholderText('Please enter your email.');
        const email = 'me@gmail.com';
        fireEvent.change(inputNode, { target: { value: email } });
        const formNode = getByTestId('form');
        fireEvent.submit(formNode);
        await wait(() => {
          expect(handleTeamCreation).toHaveBeenCalledWith(
            userEmail,
            email,
            db,
            auth,
            send
          );
      
          expect(handleTeamCreation).toHaveBeenCalledTimes(1);
        });
      });
      

      并以同样的方式模拟了firestore where查询。

      test('Invite form must contain a valid email address', async () => {
        const send = jest.fn();
        const db = jest.fn();
        const dbWhere = jest.fn().mockResolvedValue([]);
      
        const { getByPlaceholderText, queryByTestId } = render(
          <Invite send={send} db={db} dbWhere={dbWhere} />
        );
        expect(queryByTestId('error')).not.toBeInTheDocument();
        const inputNode = getByPlaceholderText('Please enter your email.');
        const email = 'x';
        fireEvent.change(inputNode, { target: { value: email } });
      
        await wait(() => {
          expect(queryByTestId('error')).toHaveTextContent('Invalid email address');
        });
      });
      

      这很简单,但是很有效。它也很冗长,但我认为一个真实的用例会比一个人为的例子更有帮助。我希望这对某人有所帮助。

      【讨论】:

        【解决方案4】:

        我发现模拟导入效果很好。我在测试中添加了这段代码,上面我渲染了我的组件导入'firebase/app'

        jest.mock('firebase/app', () => ({
          __esModule: true,
          default: {
            apps: [],
            initializeApp: () => {},
            auth: () => {},
          },
        }));
        

        【讨论】:

        • 你能添加一些测试它的例子吗?
        【解决方案5】:

        已经有一段时间没有关于这个问题的任何活动了,但网上的资料仍然不多,这是我的解决方案:

        export default class FirestoreMock {
          constructor () {
            // mocked methods that return the class
            this.mockCollection = jest.fn(() => this)
            this.mockWhere = jest.fn(() => this)
            this.mockOrderBy = jest.fn(() => this)
        
            // methods that return promises
            this.mockAdd = jest.fn(() => Promise.resolve(this._mockAddReturn))
            this.mockGet = jest.fn(() => Promise.resolve(this._mockGetReturn))
        
            // methods that accepts callbacks
            this.mockOnSnaptshot = jest.fn((success, error) => success(this._mockOnSnaptshotSuccess))
        
            // return values
            this._mockAddReturn = null
            this._mockGetReturn = null
            this._mockOnSnaptshotSuccess = null
          }
        
          collection (c) {
            return this.mockCollection(c)
          }
        
          where (...args) {
            return this.mockWhere(...args)
          }
        
          orderBy (...args) {
            return this.mockOrderBy(...args)
          }
        
          add (a) {
            return this.mockAdd(a)
          }
        
          get () {
            return this.mockGet()
          }
        
          onSnapshot (success, error) {
            return this.mockOnSnaptshot(success, error)
          }
        
          set mockAddReturn (val) {
            this._mockAddReturn = val
          }
        
          set mockGetReturn (val) {
            this._mockGetReturn = val
          }
        
          set mockOnSnaptshotSuccess (val) {
            this._mockOnSnaptshotSuccess = val
          }
        
          reset () {
            // reset all the mocked returns
            this._mockAddReturn = null
            this._mockGetReturn = null
            this._mockOnSnaptshotSuccess = null
        
            // reset all the mocked functions
            this.mockCollection.mockClear()
            this.mockWhere.mockClear()
            this.mockOrderBy.mockClear()
            this.mockAdd.mockClear()
            this.mockGet.mockClear()
          }
        }
        

        这是一个示例用法:

        import FirestoreMock from '../test_helpers/firestore.mock'
        import firebase from 'firebase/app'
        import 'firebase/firestore'
        
        describe('The Agreement model', () => {
            const firestoreMock = new FirestoreMock()
            beforeEach(() => {
                firebase.firestore = firestoreMock
                firestoreMock.reset()
            })
        
            it('does something', (done) => {
                firestoreMock.mockAddReturn = { id: 'test-id' }
                firebase.firestore.collection('foobar')
                  .add({foo: 'bar'})
                  .then(res => {
                    expect(firestoreMock.mockCollection).toBeCalledWith('foobar')
                    expect(firestoreMock.mockAdd).toBeCalledWith({foo: 'bar'})
                    expect(res.id).toEqual('test-id')
                    done()
                  })
                  .catch(done)
            })
        })
        

        如果有任何兴趣,我可以打包 FirestoreMock 实现,以便轻松共享

        特奥

        【讨论】:

          【解决方案6】:

          我在我的一个函数中使用firebase.firestore.FieldValue.serverTimestamp()

          import firestore from '@react-native-firebase/firestore';
          import firebase from  '@react-native-firebase/app';
          
          export function getUserCreateObject() {
              return {
                  property1: {
                    property2: {
                      value1: true,
                      last_updated_date: firebase.firestore.FieldValue.serverTimestamp(),
                    },
                    //the rest of the JSON object
                  },
                };
          }
          

          为了模拟这个,我有一个 jest.setup.js 文件,我在 package.json 中引用了它:

          "jest": {
              "preset": "react-native",
              "moduleDirectories": [
                "node_modules",
                "src"
              ],
              "transform": {
                "\\.js$": "<rootDir>/node_modules/babel-jest"
              },
              "transformIgnorePatterns": [
                "/node_modules/(?!(jest-)?react-native|@react-native-firebase/auth|@react-native-firebase/app|@react-native-firebase/app-types)"
              ],
              "setupFiles": [
                "./jest/jest.setup.js"
              ],
              "coveragePathIgnorePatterns": [
                "/node_modules/",
                "/jest"
              ]
            }
          

          在 jest.setup.js 中我会这样做:

          jest.mock('@react-native-firebase/app', () => ({
              firestore: {
                FieldValue: {
                  serverTimestamp: jest.fn(),
                }
              }
          }));
          

          【讨论】:

            【解决方案7】:

            如果嘲笑看起来很乏味,那就不要。使用模拟器。

            我相信这是在测试中处理读写的一个相对较新的选项,所以我发布了它。这是一个快速演练。

            1. 下载 firebase CLI 工具。
            $ curl -sL firebase.tools | bash
            
            1. 如果您还没有在项目中初始化 firebase。除非您知道自己需要其他人,否则只需选择 firestore 即可开始使用。
            $ firebase init
            
            1. 将您的 firestore 实例配置为指向模拟器(您应该能够为重定向到模拟器的那个模拟 db,但这种方式也允许您在开发环境中读取/写入模拟器)。李>
            const db = firebase.initializeApp(config).firestore()
            if (location.hostname === "localhost") {
              db.settings({
                host: "localhost:8080",
                ssl: false
              });
            }
            
            1. 启动模拟器。还有一个命令可以在 shell 命令期间运行模拟器,如果您愿意,可以将其添加到您的测试套件 npm 脚本中。
            $ firebase emulators:start
            
            1. 测试使用 firestore 的东西。
              describe('New city', () => {
                it('should create a new city in firestore', async () => {
                  await db.collection('cities').doc('Seattle').set({ state: "WA" })
                  const city = await db.collection('cities').doc("Seattle").get()
            
                  expect(city.data()['population']).toEqual("WA")
                })
              })
            
            1. 可选:创建一个数据库清理函数,该函数使用模拟器的 rest 端点在测试之间删除数据。
            async function cleanFirestore() {
              const Http = new XMLHttpRequest();
              const url = "http://localhost:8080/emulator/v1/projects/<YOUR-PROJECT-ID>/databases/(default)/documents"
            
              Http.open("DELETE", url);
              Http.send();
            
              return new Promise((resolve, reject) => {
                setTimeout(reject, 2000)
                Http.onreadystatechange = resolve
              })
            }
            

            有关 Google 的模拟器演练指南: https://google.dev/pathways/firebase-emulators

            文档: https://firebase.google.com/docs/emulator-suite

            【讨论】:

            • 设置集成测试的好方法。但是如果我们想做单元测试,模拟是必不可少的。
            • 同意。使用模拟器会对速度产生不利影响。
            猜你喜欢
            • 1970-01-01
            • 2020-03-31
            • 2020-11-10
            • 1970-01-01
            • 1970-01-01
            • 2016-05-20
            • 2023-03-26
            • 2021-12-08
            • 2018-06-21
            相关资源
            最近更新 更多