如何解决使用 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 相同。
这是我要测试的功能:
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 举报,一经查实,本站将立刻删除。