Как издеваться над методами Firebase Firestore с помощью Jest?

У меня есть ряд функций, каждая из которых выполняет различные взаимодействия с хранилищем огня. Как мне использовать 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 сначала вызывается как функция, а затем упоминается как свойство. Я считаю, что это источник моих проблем.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
17
0
20 486
7

Ответы 7

Вот решение, которое я нашел. В Интернете не так много информации об этом, поэтому я надеюсь, что это кому-то поможет.

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))
            }
          })
        }
      })
    }
  })

}

Спасибо, как я могу проверить, например, в коллекции firestore, полученной с определенным условием where

Reza 01.08.2019 16:26

что-то вроде этого expect(collection.where).toHaveBeenCalledWith('assignedNumbe‌​rs.123', '==', true); я имею ввиду

Reza 01.08.2019 19:12

Я использовал подход внедрения зависимостей для компонентов, и это означало, что я мог имитировать и тестировать методы без всего шаблона.

Например, у меня есть компонент формы, который обрабатывает приглашения следующим образом:

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(),
      }
    }
}));

Если насмешки кажутся утомительными, не делайте этого. Используйте эмуляторы.

Я считаю, что это относительно новый вариант обработки операций чтения и записи при тестировании, поэтому я публикую его. Вот краткий обзор.

  1. Загрузите инструменты командной строки firebase.
$ 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. Запустите эмулятор. Также есть команда, которая запускает эмуляторы на время выполнения команды оболочки, которую вы можете добавить в свой сценарий 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. Необязательно: создайте функцию очистки базы данных, которая использует конечную точку отдыха эмулятора для удаления данных между тестами.
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

Хороший способ настройки интеграционных тестов. Но насмешки необходимы, если мы хотим проводить модульное тестирование.

Andrey Gordeev 14.06.2021 08:30

Другие вопросы по теме