用Mocha延迟测试Cloud Functions的内容
Delay content of test of Cloud Functions with Mocha
我正在尝试测试与 Firestore 数据库交互的 Cloud Function。我正在关注在线模式下使用 firebase-functions-test 和 mocha (https://firebase.google.com/docs/functions/unit-testing)
测试我的功能的文档
由于我要测试的功能是删除集合中的文档,因此我首先在测试中创建了一个推送到数据库的假文档。
然后我用包装函数调用来测试。它是异步的,所以需要一点时间。
我要验证的是文档已被正确删除。但是调用获取文档然后断言有时比我执行删除的实际函数更快。我想在进行验证之前添加一个小延迟。
我试图添加一个settimeout,但是测试returns 'passed'没有等待超时里面的代码到运行并检查断言。
这是我的测试。任何帮助将不胜感激!
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;
});
}
我不太确定 .wrap()
是什么。那 return 是一个承诺吗?
如果你想使用.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 秒运行一次,如果任务仍未完成,它会自行重复。
您看到并错误地尝试解决的奇怪行为是由您错误实施的 Cloud Function 造成的。
目前您的函数执行以下操作:
- 查找所有早于 7 天的通知并对每个通知开始删除操作。
- 结束 Cloud Function,无需等待上述操作完成。
第二点是为什么您在删除数据库中的数据之前必须等待几秒钟。
在已部署的函数中,一旦函数 returns、所有进一步的操作都应视为永远不会执行 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 通过使用字段仅获取文档 ID 列表(无内部文档数据)来提高效率["__name__"]
的掩码和适当的查询参数。
通过上述任一修复,您的测试将变为:
// run the function
await wrapped();
// check function result
const deletedDocSnapshot = await admin.firestore()
.collection('notifications')
.doc(notificationToDeleteId)
.get();
assert.equal(deletedDocSnapshot.data(), null);
但是,为了也回答原来的问题,这个函数将创建一个可等待的 setTimeout:
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);
});
}
然后使用它:
// run the function
await wrapped();
await setTimeoutPromise(() => {
return admin.firestore()
.collection('notifications')
.doc(notificationToDeleteId)
.get()
.then((deleteDoc) => {
//console.log(deleteDoc.data());
assert.equal(deleteDoc.data(), null);
});
}, 5000);
我正在尝试测试与 Firestore 数据库交互的 Cloud Function。我正在关注在线模式下使用 firebase-functions-test 和 mocha (https://firebase.google.com/docs/functions/unit-testing)
测试我的功能的文档由于我要测试的功能是删除集合中的文档,因此我首先在测试中创建了一个推送到数据库的假文档。 然后我用包装函数调用来测试。它是异步的,所以需要一点时间。
我要验证的是文档已被正确删除。但是调用获取文档然后断言有时比我执行删除的实际函数更快。我想在进行验证之前添加一个小延迟。
我试图添加一个settimeout,但是测试returns 'passed'没有等待超时里面的代码到运行并检查断言。
这是我的测试。任何帮助将不胜感激!
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;
});
}
我不太确定 .wrap()
是什么。那 return 是一个承诺吗?
如果你想使用.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 秒运行一次,如果任务仍未完成,它会自行重复。
您看到并错误地尝试解决的奇怪行为是由您错误实施的 Cloud Function 造成的。
目前您的函数执行以下操作:
- 查找所有早于 7 天的通知并对每个通知开始删除操作。
- 结束 Cloud Function,无需等待上述操作完成。
第二点是为什么您在删除数据库中的数据之前必须等待几秒钟。
在已部署的函数中,一旦函数 returns、所有进一步的操作都应视为永远不会执行 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 通过使用字段仅获取文档 ID 列表(无内部文档数据)来提高效率["__name__"]
的掩码和适当的查询参数。
通过上述任一修复,您的测试将变为:
// run the function
await wrapped();
// check function result
const deletedDocSnapshot = await admin.firestore()
.collection('notifications')
.doc(notificationToDeleteId)
.get();
assert.equal(deletedDocSnapshot.data(), null);
但是,为了也回答原来的问题,这个函数将创建一个可等待的 setTimeout:
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);
});
}
然后使用它:
// run the function
await wrapped();
await setTimeoutPromise(() => {
return admin.firestore()
.collection('notifications')
.doc(notificationToDeleteId)
.get()
.then((deleteDoc) => {
//console.log(deleteDoc.data());
assert.equal(deleteDoc.data(), null);
});
}, 5000);