如何进行集成测试 NodeJS + Firebase Admin?

How to do Integration tests NodeJS + Firebase Admin?

我正在尝试使用 Firebase 在 NodeJS 上编写一些集成测试(firebase-admin,带有测试库 jest 和 supertest),当我 运行 我所有的测试时,一些测试随机失败。另外,我的测试通过了,但似乎当太多测试用例 运行ning 时,一些 api 调用失败了。这里有人有这样的问题吗?这个问题的解决方案是什么?是什么导致了这个问题? (注意:我 运行 我的测试顺序是为了不混淆我的数据库的初始化。我使用选项 --运行InBand 开玩笑)

有一些可用的模拟库,但它们似乎与旧的 api firebase 一起使用。

另一种解决方案是模拟我所有操作 firebase 的函数,但我不会再进行 "real" 集成测试,这意味着要编写大量额外的代码来编写这些模拟。这样做是最佳做法吗?

提前致谢!


编辑:代码片段:

initTest.js:

const request = require('supertest');
const net = require('net');
const app = require('../src/server').default;

export const initServer = () => {
    const server = net.createServer(function(sock) {
      sock.end('Hello world\n');
    });
    return server
}

export const createAdminAndReturnToken = async (password) => {
    await request(app.callback())
        .post('/admin/users/sa')
        .set('auth','')
        .send({password});
    // user logs in
    const res = await request(app.callback())
        .post('/web/users/login')
        .set('auth','')
        .send({email:"sa@optimetriks.com",password})
    return res.body.data.token;
}

utils.ts:

import firestore from "../../src/tools/firestore/index";

export async function execOperations(operations,action,obj) {
    if (process.env.NODE_ENV === "test") {
      await Promise.all(operations)
        .then(() => {
          console.log(action+" "+obj+" in database");
        })
        .catch(() => {
          console.log("Error", "error while "+action+"ing "+obj+" to database");
        });
    } else {
      console.log(
        "Error",
        "cannot execute this action outside from the test environment"
      );
    }
  }

  //////////////////////// Delete collections ////////////////////////

export async function deleteAllCollections() {
    const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
    collections.forEach(collection => {
      deleteCollection(collection);
    });
  }

  export async function deleteCollection(collectionPath) {
    const batchSize = 10;
    var collectionRef = firestore.collection(collectionPath);
    var query = collectionRef.orderBy("__name__").limit(batchSize);

    return await new Promise((resolve, reject) => {
      deleteQueryBatch(firestore, query, batchSize, resolve, reject);
    });
  }

 async function deleteQueryBatch(firestore, query, batchSize, resolve, reject) {
    query
      .get()
      .then(snapshot => {
        // When there are no documents left, we are done
        if (snapshot.size == 0) {
          return 0;
        }

        // Delete documents in a batch
        var batch = firestore.batch();
        snapshot.docs.forEach(doc => {
          batch.delete(doc.ref);
        });

        return batch.commit().then(() => {
          return snapshot.size;
        });
      })
      .then(numDeleted => {
        if (numDeleted === 0) {
          resolve();
          return;
        }

        // Recurse on the next process tick, to avoid
        // exploding the stack.
        process.nextTick(() => {
          deleteQueryBatch(firestore, query, batchSize, resolve, reject);
        });
      })
      .catch(reject);
  }

populateClient.ts:

import firestore from "../../src/tools/firestore/index";
import {execOperations} from "./utils";
import { generateClientData } from "../factory/clientFactory";

jest.setTimeout(10000); // some actions here needs more than the standard 5s timeout of jest

// CLIENT
export async function addClient(client) {
    const clientData = await generateClientData(client);
    await firestore
        .collection("clients")
        .doc(clientData.id)
        .set(clientData)
}

export async function addClients(clientNb) {
  let operations = [];
  for (let i = 0; i < clientNb; i++) {
    const clientData = await generateClientData({});
    operations.push(
      await firestore
        .collection("clients")
        .doc(clientData.id)
        .set(clientData)
    );
  }
  await execOperations(operations,"add","client");
}

retrieveClient.ts:

import firestore from "../../src/tools/firestore/index";
import { resolveSnapshotData } from "../../src/tools/tools";

export async function getAllClients() {
    return new Promise((resolve, reject) => {
        firestore
          .collection("clients")
          .get()
          .then(data => {
            resolveSnapshotData(data, resolve);
          })
          .catch(err => reject(err));
      });
}

clients.test.js:

const request = require('supertest');
const app = require('../../../src/server').default;
const {deleteAllCollections, deleteCollection} = require('../../../__utils__/populate/utils')
const {addClient} = require('../../../__utils__/populate/populateClient')
const {getAllClients} = require('../../../__utils__/retrieve/retrieveClient')
const {initServer,createAdminAndReturnToken} = require('../../../__utils__/initTest');
const faker = require('faker');

let token_admin;
let _server;
// for simplicity, we use the same password for every users
const password = "secretpassword";

beforeAll(async () => {
    _server = initServer(); // start
    await deleteAllCollections()
    // create a super admin, login and store the token
    token_admin = await createAdminAndReturnToken(password);
    _server.close();   // stop
})

afterAll(async () => {
    // remove the users created during the campaign
    _server = initServer(); // start
    await deleteAllCollections()
    _server.close();   // stop
})

describe('Manage client', () => {

    beforeEach(() => {
        _server = initServer(); // start
    })

    afterEach(async () => {
        await deleteCollection("clients")
        _server.close();   // stop
    })

    describe('Get All clients', () => {

        const exec = (token) => {
            return request(app.callback())
            .get('/clients')
            .set('auth',token)
        }

        it('should return a 200 when super admin provide the action', async () => {
            const res = await exec(token_admin);
            expect(res.status).toBe(200);
        });

        it('should contain an empty array while no client registered', async () => {
            const res = await exec(token_admin);
            expect(res.body.data.clients).toEqual([]);
        });

        it('should contain an array with one item while a client is registered', async () => {
            // add a client
            const clientId = faker.random.uuid();
            await addClient({name:"client name",description:"client description",id:clientId})
            // call get clients and check the result
            const res = await exec(token_admin);
            expect(res.body.data.clients.length).toBe(1);
            expect(res.body.data.clients[0]).toHaveProperty('name','client name');
            expect(res.body.data.clients[0]).toHaveProperty('description','client description');
            expect(res.body.data.clients[0]).toHaveProperty('id',clientId);
        });
    })

    describe('Get client by ID', () => {

        const exec = (token,clientId) => {
            return request(app.callback())
            .get('/clients/' + clientId)
            .set('auth',token)
        }

        it('should return a 200 when super admin provide the action', async () => {
            const clientId = faker.random.uuid();
            await addClient({id:clientId})
            const res = await exec(token_admin,clientId);
            expect(res.status).toBe(200);
        });

        it('should return a 404 when the client does not exist', async () => {
            const nonExistingClientId = faker.random.uuid();
            const res = await exec(token_admin,nonExistingClientId);
            expect(res.status).toBe(404);
        });
    })

    describe('Update client', () => {

        const exec = (token,clientId,client) => {
            return request(app.callback())
            .patch('/clients/' + clientId)
            .set('auth',token)
            .send(client);
        }

        const clientModified = {
            name:"name modified",
            description:"description modified",
            app_user_licenses: 15
        }

        it('should return a 200 when super admin provide the action', async () => {
            const clientId = faker.random.uuid();
            await addClient({id:clientId})
            const res = await exec(token_admin,clientId,clientModified);
            expect(res.status).toBe(200);
            // check if the client id modified
            let clients = await getAllClients();
            expect(clients.length).toBe(1);
            expect(clients[0]).toHaveProperty('name',clientModified.name);
            expect(clients[0]).toHaveProperty('description',clientModified.description);
            expect(clients[0]).toHaveProperty('app_user_licenses',clientModified.app_user_licenses);
        });

        it('should return a 404 when the client does not exist', async () => {
            const nonExistingClientId = faker.random.uuid();
            const res = await exec(token_admin,nonExistingClientId,clientModified);
            expect(res.status).toBe(404);
        });
    })

    describe('Create client', () => {

        const exec = (token,client) => {
            return request(app.callback())
            .post('/clients')
            .set('auth',token)
            .send(client);
        }

        it('should return a 200 when super admin does the action', async () => {
            const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
            expect(res.status).toBe(200);
        });

        it('list of clients should be appended when a new client is created', async () => {
            let clients = await getAllClients();
            expect(clients.length).toBe(0);
            const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
            expect(res.status).toBe(200);
            clients = await getAllClients();
            expect(clients.length).toBe(1);
            expect(clients[0]).toHaveProperty('name','clientA');
            expect(clients[0]).toHaveProperty('description','description for clientA');
        });
    });

    describe('Delete client', () => {

        const exec = (token,clientId) => {
            return request(app.callback())
            .delete('/clients/'+ clientId)
            .set('auth',token);
        }

        it('should return a 200 when super admin does the action', async () => {
            const clientId = faker.random.uuid();
            await addClient({id:clientId})
            const res = await exec(token_admin,clientId);
            expect(res.status).toBe(200);
        });

        it('should return a 404 when trying to delete a non-existing id', async () => {
            const clientId = faker.random.uuid();
            const nonExistingId = faker.random.uuid();
            await addClient({id:clientId})
            const res = await exec(token_admin,nonExistingId);
            expect(res.status).toBe(404);
        });

        it('the client deleted should be removed from the list of clients', async () => {
            const clientIdToDelete = faker.random.uuid();
            const clientIdToRemain = faker.random.uuid();
            await addClient({id:clientIdToRemain})
            await addClient({id:clientIdToDelete})
            let clients = await getAllClients();
            expect(clients.length).toBe(2);
            await exec(token_admin,clientIdToDelete);
            clients = await getAllClients();
            expect(clients.length).toBe(1);
            expect(clients[0]).toHaveProperty('id',clientIdToRemain);
        });
    });
})

开玩笑命令:jest --coverage --forceExit --runInBand --collectCoverageFrom=src/**/*ts

我发现了问题:我在 "deleteAllCollection" 函数上遇到了问题,我忘了输入 "await"。

下面是这个函数的更正:

export async function deleteAllCollections() {
    const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
    for (const collection of collections) {
      await deleteCollection(collection);
    };
  }