У меня есть ряд функций, каждая из которых выполняет различные взаимодействия с хранилищем огня. Как мне использовать 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 [JS]](https://i.imgur.com/WsjO6zJb.png)


Вот решение, которое я нашел. В Интернете не так много информации об этом, поэтому я надеюсь, что это кому-то поможет.
EDIT: I believe you can do something similar using jests
/__MOCKS__/folders system, rather than overwriting the firestore object as I do in this example.
Хитрость заключается в том, чтобы создать связанный API фиктивных функций и установить его в объекте firebase вместо импорта и имитации хранилища огня. Приведенный ниже пример позволяет мне протестировать приведенную выше функцию примера, а также обещания 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 };
Я объявляю его в файле, который запускается до того, как будут выполнены все мои тесты (см. Документацию), и импортирую и использую его в своих тестовых файлах следующим образом:
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"
});
});
});
Вот как я для шутки издевался над 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))
}
})
}
})
}
})
}
что-то вроде этого expect(collection.where).toHaveBeenCalledWith('assignedNumbers.123', '==', true); я имею ввиду
Я использовал подход внедрения зависимостей для компонентов, и это означало, что я мог имитировать и тестировать методы без всего шаблона.
Например, у меня есть компонент формы, который обрабатывает приглашения следующим образом:
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.handleChangeEvent}
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 = '[email protected]';
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 = '[email protected]';
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 query.
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');
});
});
Это очень просто, но работает. Это также довольно многословно, но я подумал, что реальный вариант использования будет более полезным, чем надуманный пример. Я надеюсь, что это помогает кому-то.
Я обнаружил, что насмешка над импортом работает хорошо. Я добавил этот код в тест, выше, где я визуализирую свой компонент, импортирующий firebase / app.
jest.mock('firebase/app', () => ({
__esModule: true,
default: {
apps: [],
initializeApp: () => {},
auth: () => {},
},
}));
Прошло некоторое время с момента появления каких-либо действий по этому вопросу, но в Интернете все еще не так много материалов, вот мое решение:
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, чтобы ею можно было легко поделиться
Тео
Я использую 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(),
}
}
}));
Если насмешки кажутся утомительными, не делайте этого. Используйте эмуляторы.
Я считаю, что это относительно новый вариант обработки операций чтения и записи при тестировании, поэтому я публикую его. Вот краткий обзор.
$ curl -sL firebase.tools | bash
$ firebase init
const db = firebase.initializeApp(config).firestore()
if (location.hostname === "localhost") {
db.settings({
host: "localhost:8080",
ssl: false
});
}
$ firebase emulators:start
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")
})
})
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
Хороший способ настройки интеграционных тестов. Но насмешки необходимы, если мы хотим проводить модульное тестирование.
Спасибо, как я могу проверить, например, в коллекции firestore, полученной с определенным условием
where