使用 Mocha 延迟测试 Cloud Functions 的内容

如何解决使用 Mocha 延迟测试 Cloud Functions 的内容

我正在尝试测试与 Firestore 数据库交互的云函数。我正在关注在线模式下使用 firebase-functions-test 和 mocha (https://firebase.google.com/docs/functions/unit-testing) 测试我的功能的文档

因为我想测试删除集合中的文档的功能,所以我首先在测试中创建了一个我推送到数据库的假文档。 然后我调用了我的函数来测试。它是异步的,所以需要一点时间。

我想验证的是文档是否已被正确删除。但是调用获取文档然后断言有时比我执行删除的实际函数更快。我想在进行验证之前添加一个小的延迟。

我尝试添加 settimeout,但测试返回“通过”,而无需等待超时内的代码运行并检查断言。

这是我的测试。任何帮助将不胜感激!

const myFunctions = require('../src/delete_notification.ts');

    it('delete notification of db more than 7 days old',async() => {

        // create test notification in db
        const notificationToDeleteId = 'TEST_1234567890';
        const notificationToDelete = {
            uid: notificationToDeleteId,createdOn: '2021-01-01T00:00:00.00000'
        };

        await admin.firestore().collection('notifications')
                 .doc(notificationToDeleteId)
                 .set(notificationToDelete);

        // call the cloud function
        const wrapped = test.wrap(myFunctions.deleteNotificationAfter7Days);
        await wrapped();

        //this code needs to be delayed by a few seconds

        return admin.firestore()
            .collection('notifications')
            .doc(notificationToDeleteId).get().then((deleteDoc) => {
                //console.log(deleteDoc.data());
                assert.equal(deleteDoc.data(),null);
        });
    }); 

更新:

我试过这个代码

return wrapped().then(() => {
            return setTimeout(() => {
              return admin.firestore().collection('notifications').doc(notificationToDeleteId)
                  .get().then((deleteDoc) => {
                      console.log(deleteDoc.data());
                      assert.equal(deleteDoc.data(),null);
                  });
            },5000)
        });

同时确保我调用函数不会删除文档。就像那样,我希望我的测试失败。但是使用下面的这段代码(带有 settimeout),测试一直通过。与 setInterval 相同。

enter image description here

这是我要测试的功能

 import * as functions from 'firebase-functions';
    import * as admin from 'firebase-admin';
    
    if (admin.apps.length === 0) admin.initializeApp();
    const db = admin.firestore();
    
    // run every  7 days
    export const deleteNotificationAfter7Days = functions
    .region('europe-west6')
    .pubsub
    .schedule('every 24 hours')
    .timeZone('Africa/Accra')
    .onRun(async context => {
    
        const currentDate = new Date();
        const currentDateMinus7Days = new Date(currentDate.getTime() - 604800000);
        const currentDateMinus7Daysstring = currentDateMinus7Days.toISOString();
    
        //console.log("date minus 7 days " + currentDateMinus7Daysstring);
    
       try {
            const querySnapshot = await db.collection('notifications').where("createdOn","<",currentDateMinus7Daysstring).get();
            if(querySnapshot.empty) return;
    
            querySnapshot.forEach( async function(doc){
           

     const notificationId = doc.id;

            //console.log('notificationId id ' + notificationId);
            await deleteNotification(notificationId);
            return;
        });
    } catch (error) {
        console.log('Error deleting notifications ' + error);
    }
    return;
});

async function deleteNotification(notificationId : string) {

    //console.log('DELETE FROM NOTIFICATION');

    return db.collection('notifications')
        .doc(notificationId)
        .delete()
        .then(function() {
            console.log('Notification deleted');
        })
        .catch(function(error) {
            throw error;
        });
}

解决方法

您看到并错误地尝试解决的奇怪行为是由您错误地实施的云函数引起的。

目前您的函数执行以下操作:

  • 查找超过 7 天的所有通知,并为每个通知开始删除操作。
  • 无需等待上述操作完成即可结束 Cloud Function。

第二点是为什么在删除数据库中的数据之前必须等待几秒钟。

在已部署的函数中,一旦函数返回,所有进一步的操作都应该被视为永远不会执行 as documented here。 “非活动”功能可能随时终止,受到严重限制,您发出的任何网络调用(如删除文档)可能永远不会执行。


在您的代码中,您使用 const notificationId = doc.id; deleteNotificationId(notificationId) 删除通知。这可以用 doc.ref.delete() 代替以达到相同的目的。

要修复您的函数,我们需要等待删除操作完成,然后才能从函数返回并结束其生命周期。

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
    
if (admin.apps.length === 0) admin.initializeApp();
const db = admin.firestore();
    
export const deleteNotificationAfter7Days = functions
  .region('europe-west6')
  .pubsub
  .schedule('every 24 hours')
  .timeZone('Africa/Accra')
  .onRun(async context => {
    
    const currentDateMinus7Days = new Date(Date.now() - 604800000);
    const currentDateMinus7DaysString = currentDateMinus7Days.toISOString();
    
    try {
      const querySnapshot = await db.collection('notifications').where("createdOn","<",currentDateMinus7DaysString).get();
      if (querySnapshot.empty) {
        console.log("No notification documents to clean up. Aborted.");
        return;
      }
    
      const deleteDocPromises = [];

      querySnapshot.forEach(function (doc) {
        deleteDocPromises.push(doc.ref.delete());
      });

      // wait for all operations to complete
      await Promise.all(deleteDocPromises);

      console.log("All old notifications cleaned up successfully.");
    } catch (error) {
      console.log('Unexpected error deleting old notifications: ' + error);
    }
});

使用你当前的函数(包括上面的代码),如果任何单个文档的删除操作失败,整个函数就会崩溃。虽然您可以捕获错误以免发生这种情况,但如果您有 200 个要删除的文档并且它们都失败了,那么您将有 200 个错误和 200 个失败的网络请求。相反,您应该设置一个阈值,以便在出现 X 个错误后它会使函数崩溃。这允许一些失败,同时仍然删除其他人,而那些失败的将在下次函数运行时重新尝试。

const deleteDocPromises = [];

let errorCount = 0;
const handleError = (error: any) => {
  if (++errorCount > 10) {
    throw new Error("Error threshold exceeded");
  return error;
};

querySnapshot.forEach(function (doc) {
  deleteDocPromises.push(
    doc.ref
      .delete()
      .catch(handleError)
  );
});

另一个改进是使用批处理来执行删除,以减少函数的网络开销:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
    
if (admin.apps.length === 0) admin.initializeApp();
const db = admin.firestore();
    
export const deleteNotificationAfter7Days = functions
  .region('europe-west6')
  .pubsub
  .schedule('every 24 hours')
  .timeZone('Africa/Accra')
  .onRun(async context => {
    
    const currentDateMinus7Days = new Date(Date.now() - 604800000);
    const currentDateMinus7DaysString = currentDateMinus7Days.toISOString();
    
    try {
      const querySnapshot = await db.collection('notifications').where("createdOn",currentDateMinus7DaysString).get();
      if (querySnapshot.empty) {
        console.log("No notification documents to clean up. Aborted.");
        return;
      }
    
      let currentBatch = db.batch(),currentBatchCount = 0;
      const batches = [currentBatch];
   
      // for each document,queue its deletion
      querySnapshot.forEach(function (doc) {
        if (++currentBatchCount > 500) {
          // more than 500 operations in the current batch,start a new one
          currentBatch = db.batch();
          currentBatchCount = 1;
          batches.push(currentBatch);
        }

        currentBatch.delete(doc.ref);
      });

      // wait for all operations to complete
      const batchErrors = await Promise.all(batches.map(b => {
        return b.commit()
          .then(
            () => null,(error) => error // trap errors so other batches can still complete
          );
      }));

      const errorCodeSummary: Record<string,number> = {};
      let errorCount = 0;

      batchErrors
        .forEach((error) => {
          if (error === null)
            return;

          errorCount++;
          const errorCode = error.code || "unknown";
          errorCodeSummary[errorCode] = (errorCodeSummary[errorCode] || 0) + 1;
        });

      if (errorCount > 0) {
        console.error(
          `${errorCount}/${batches.length} batches failed while cleaning up old notifications. ` +
          `They had these error codes: ${JSON.stringify(errorCodeSummary)}`
        );
      } else {
        console.log("All old notifications cleaned up successfully.");
      }
    } catch (error) {
      console.log('Unexpected error deleting old notifications: ' + error);
    }
});

注意:您可以通过使用 REST API's List 通过使用字段掩码 {{1 }} 和适当的查询参数。


通过上述任一修复,您的测试将变为:

["__name__"]

然而,为了回答原来的问题,这个函数将创建一个可等待的 setTimeout:

// run the function
await wrapped();

// check function result
const deletedDocSnapshot = await admin.firestore()
  .collection('notifications')
  .doc(notificationToDeleteId)
  .get();

assert.equal(deletedDocSnapshot.data(),null);

然后使用它:

function setTimeoutPromise(callback: (...args: any[]) => any | Promise<any>,timeoutMS: number,...args: any[]) {
  return new Promise((resolve,reject) => {
    setTimeout((...args) => {
      try {
        Promise.resolve(callback(...args))
          .then(resolve,reject); // <-- handles Promise-based errors
      } catch (err) {
        reject(err); // <-- handles errors if `callback()` isn't returning a Promise
      }
    },timeoutMS,...args);
  });
}
,

我不太确定 .wrap() 是什么。这会返回一个承诺吗? 如果你想使用.setTimeout(),你可以这样试试:

const wrapped = test.wrap(myFunctions.deleteNotificationAfter7Days);
await wrapped();

setTimeout(() => {
  return admin.firestore().collection('notifications').doc(notificationToDeleteId)
      .get().then((deleteDoc) => {
          //console.log(deleteDoc.data());
          assert.equal(deleteDoc.data(),null);
      });
},5000)

或者您可以使用 setInterval(),它每 X 秒运行一次,如果任务仍未完成,则自行重复。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?