您如何使用Jest模拟Firebase Firestore方法?

Bre*_*ill 4 javascript firebase jestjs google-cloud-firestore

我有一系列功能,每个功能执行各种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);
};
Run Code Online (Sandbox Code Playgroud)

请注意,如何照常导入firebase,然后导入firestore的副作用。还要注意,如何首先将Firestore调用为函数,然后再引用为属性。我相信这是我麻烦的根源。

teo*_*one 11

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

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()
  }
}
Run Code Online (Sandbox Code Playgroud)

这是一个示例用法:

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)
    })
})
Run Code Online (Sandbox Code Playgroud)

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

特奥


Bre*_*ill 7

这是我找到的解决方案。网上没有太多有关此的信息,所以希望对您有所帮助。

诀窍是创建模拟函数的链接API,并将其设置在firebase对象上,而不是导入和模拟firestore。下面的示例使我可以测试上述示例功能,并且可以doc().get()保证。

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 };
Run Code Online (Sandbox Code Playgroud)

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

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"
    });
  });
});
Run Code Online (Sandbox Code Playgroud)


小智 7

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

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

  1. 下载 Firebase CLI 工具。
$ curl -sL firebase.tools | bash
Run Code Online (Sandbox Code Playgroud)
  1. 如果您还没有在您的项目中初始化 firebase。除非您知道您需要其他人,否则只需选择 firestore 即可开始使用。
$ firebase init
Run Code Online (Sandbox Code Playgroud)
  1. 将您的 firestore 实例配置为指向模拟器(您应该能够为一个重定向到模拟器的模拟数据库,但这种方式也将允许您在您的开发环境中读/写模拟器)。
const db = firebase.initializeApp(config).firestore()
if (location.hostname === "localhost") {
  db.settings({
    host: "localhost:8080",
    ssl: false
  });
}
Run Code Online (Sandbox Code Playgroud)
  1. 启动模拟器。还有一个命令可以在 shell 命令的持续时间内运行模拟器,如果您愿意,可以将其添加到测试套件 npm 脚本中。
$ firebase emulators:start
Run Code Online (Sandbox Code Playgroud)
  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")
    })
  })
Run Code Online (Sandbox Code Playgroud)
  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
  })
}
Run Code Online (Sandbox Code Playgroud)

对于来自 Google 的模拟器演练指南:https : //google.dev/pathways/firebase-emulators

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

  • 设置集成测试的好方法。但如果我们想做单元测试,模拟是必不可少的。 (11认同)

小智 5

这是我嘲笑 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))
            }
          })
        }
      })
    }
  })

}
Run Code Online (Sandbox Code Playgroud)


Jos*_*man 5

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

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

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;
Run Code Online (Sandbox Code Playgroud)

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}
 />
Run Code Online (Sandbox Code Playgroud)

然后,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);
  });
});
Run Code Online (Sandbox Code Playgroud)

并以相同的方式嘲笑 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');
  });
});
Run Code Online (Sandbox Code Playgroud)

这很简单,但是很有效。它也很冗长,但我认为真实的用例比人为的示例更有帮助。我希望这可以帮助别人。