在Parse Cloud Code中集成测试:自动化指南
如何将测试集成到您的 Parse Cloud Code 函数中
介绍
这是由 约翰·康西丁, 我们的特邀作者和 K-Optional. 本教程涵盖了如何为您的 Back4App Cloud Code 设置自动化测试。
我们将简要讨论将一些客户端 Parse 代码移至云端,然后讨论如何将您的项目与测试生态系统集成。您还可以直接查看 示例项目 以获取一个可工作的版本。
目标
我们希望将自动化测试的强大、可扩展的方面与开发者友好的 Parse 环境相结合。通过利用 Cloud Code,或许是 Parse 中一个被低估的特性,开发者可以继续快速迭代他们的代码,并且可以确信软件将按预期运行。
测试驱动开发是一个广阔的领域;与其哲学性地讨论测试,我们将通过一个实现来讲解一些策略(例如,存根)。
前提条件
要完成本教程,您需要:
- 在 Back4App 上的应用程序。
- Back4App 命令行配置与项目
- 在您的命令行中安装 npm
注意:此库将使用 JavaScript Promise, 这应该不会太复杂
让我们创建一个基本的社交媒体后端

好的!想象一个社交媒体应用程序,它包括一个与用户模型相配合的个人资料模型。对于某些应用程序,您可以将个人资料信息放在用户模型中,尽管在许多情况下这并不高效;您通常需要将授权/身份验证与用户内容的关注点分开,因此维护两个不同的模型。
在本教程中,我们将实现一个功能,以这种方式管理用户和个人资料的创建,给客户端带来最小的压力。让我们开始吧!
1. 定义我们的函数
这假设您已经创建了一个 Back4App 项目,并安装了命令行工具(请参阅先决条件)。
为了简单起见,本指南将参考 Parse JavaScript SDK 语法作为前端代码的示例
当有人注册此应用程序时,应该创建一个个人资料并与用户对象关联。
注册功能
在许多 Parse 应用程序中,您将使用以下语法创建用户
1 var user = new Parse.User();
2 user.set("username", "my name");
3 user.set("password", "my pass");
4 user.set("email", "[email protected]");在我们的案例中,我们还希望初始化一个 Profile,并将其指向 User 对象。
1 try {
2 await user.signUp(null, {useMasterKey : true});
3 let Profile = Parse.Object.extend("Profile");
4 let profile = new Profile({
5 firstname : params.firstname,
6 lastname : params.lastname,
7 user : user
8 })
9 return profile.save(null, {useMasterKey : true});
10 } catch (err){
11 return (err.message);
12 }您可以将该语法缩短为类似这样的内容:
1 let user = new Parse.User({
2 username : params.username,
3 password : params.password,
4 email : params.email
5 });
6
7 try {
8 await user.signUp(null, {useMasterKey : true});
9 let Profile = Parse.Object.extend("Profile");
10 let profile = new Profile({
11 firstname : params.firstname,
12 lastname : params.lastname,
13 user : user
14 })
15 return profile.save(null, {useMasterKey : true});
16 } catch (err){
17 return (err.message);
18 }不幸的是,这仍然涉及向 Parse 服务器发出两个单独的请求,这对前端来说效率低下;在可能的情况下,避免多步骤的客户端-服务器通信流程是明智的。
此外,关于安全性,上述代码将创建过程放在客户端手中,这从来不是明智的。我们希望防止我们的数据完整性依赖于客户端正确完成流程的所有步骤。例如,他们可能会发送一个自定义请求,创建一个没有个人资料的用户,从而破坏应用程序的持久数据。
为什么不使用云代码将所有这些操作合并为一步呢?这可以防止前端代码的臃肿,并确保客户端不会进行不必要/不安全的工作!
这是我们希望从客户端进行注册的方式
1 Parse.Cloud.run('signupUser',
2 {
3 username: 'myname',
4 password : "mypass",
5 email : "[email protected]",
6 firstname : "John",
7 lastname : "Smith" }
8 ).then(function(newuser) {
9
10 });Parse 还为 beforeSave 触发器定义,允许在用户注册时创建个人资料。然而,通过使用一个函数,我们可以直观地传递个人名和姓氏属性,个人资料将使用这些属性。
云代码注册函数
让我们开始吧!移动到与 Back4App 同步的项目目录(如果您不知道这意味着什么,请查看前提条件)。我们将假设以下结构:
在我们的案例中,初始化时,我们选择了‘cloud’作为我们的目录名称。您的目录可以随意命名。
1 Parse.Cloud.define("signuserUp", function(request, response) {
2 // Make sure the necessary parameters are passed first
3 var params = request.params;
4 if (!params.username || !params.email || !params.password || !params.firstname || !params.lastname)
5 return response.error("Missing parameters: need username, email, password, firstname, & lastname");
6
7 // Execute the signup flow
8 var user = new Parse.User({
9 username : params.username,
10 password : params.password,
11 email : params.email
12 });
13 user.signUp(null, {useMasterKey : true})
14 .then((newUser) => {
15 var Profile = Parse.Object.extend("Profile");
16 var profile = new Profile({
17 firstname : params.firstname,
18 lastname : params.lastname,
19 user : newUser
20 })
21 return profile.save(null, {useMasterKey : true});
22 })
23 .then((prof) => response.success(prof))
24 .catch((e) => {
25 response.error(e.message);
26 })
27 });您可能会注意到传递了‘useMasterKey’选项;这允许云代码超越可能存在的任何角色或ACL。由于客户端不接触这段代码,因此他们劫持我们服务器的风险很小。然而,请小心使用这个标志!
如果这可能比将此功能放在客户端代码中更可取,这里有一些优势:
- 将计算卸载到服务器而不是设备上
- 明确定义过程的功能
- 更容易创建故障安全功能
- 为客户端提供直观的界面
- 这防止了客户端“半完成”一个过程的可能性。
2. 重构我们的目录结构
很好,我们已经创建了两个云函数。我们显然可以通过运行这些函数并检查Parse仪表板来测试它们,但这并不可扩展或高效。我们希望为可以持续运行的方法创建自动化测试。因此,我们将稍微分离我们的代码。
我们将把在main.js中创建的函数移动到一个名为cloud-functions.js的新文件中(在同一目录中)。然后我们将 导入这些函数到main中,并将它们绑定到云函数定义。这个想法是 解耦函数与云接口,以便我们可以在不低效地发送HTTP请求的情况下测试它们。这在我们创建测试套件时会非常有意义。
创建函数文件
您可能知道,您可以在Node.js中使用‘require’从其他文件中引入函数、对象和变量。
因此,我们将定义与我们在步骤1中创建的Parse云函数相对应的函数。
一个可能令人困惑的点是,我们正在定义的函数将是 返回函数, 然后可以连接到Parse云定义。使用一个函数返回另一个函数可能看起来很奇怪,但这将使我们在编写测试时能够更换Parse服务器。
您可能已经注意到,您可以在云代码中使用Parse对象,而无需定义或导入它。这是因为运行此代码的服务器会自动添加Parse。然而,如果我们想在本地运行函数的测试,我们并没有提供一个实例。实际上,我们希望提供一个对应于测试Parse服务器的实例,在那里创建或删除数据是没有害处的。
因此,每个函数将接受 ‘Parse’作为参数,并返回云函数。
1 // cloud-functions.js
2 module.exports.SignupUser = function(Parse) {
3 return function (request, response) {
4 // Copied from main.js:
5 // Make sure the necessary parameters are passed first
6 var params = request.params;
7 if (!params.username || !params.email || !params.password || !params.firstname || !params.lastname)
8 return response.error("Missing parameters: need username, email, password, firstname, & lastname");
9 // Execute the signup flow
10 var user = new Parse.User({
11 username : params.username,
12 password : params.password,
13 email : params.email
14 });
15 user.signUp(null, {useMasterKey : true})
16 .then((newUser) => {
17 var Profile = Parse.Object.extend("Profile");
18 var profile = new Profile({
19 firstname : params.firstname,
20 lastname : params.lastname,
21 user : newUser
22 })
23 return profile.save(null, {useMasterKey : true});
24 })
25 .then((prof) => response.success(prof))
26 .catch((e) => {
27 response.error(e.message);
28 })
29 }
30 }在main.js中,删除之前的所有内容。导入云函数,并将函数绑定到云函数定义,如下所示:
1 // main.js
2 var cloudFunctions = require("./cloud-functions");
3 // Note that we are injecting the Parse instance, which is automatically supplied in the
4 // context of Parse Cloud Code, but not on local tests
5 Parse.Cloud.define("signuserUp", cloudFunctions.SignupUser(Parse));太好了!自第一步以来,我们没有改变功能,但我们已经将函数与云代码解耦。 在下一步中,我们将创建单元测试!
3. 创建测试套件
对于我们的测试套件,我们将使用 Jasmine, 这个流行的测试框架。然而,到目前为止,我们的代码完全与我们的测试无关,因此您可以使用您喜欢的任何框架或平台。
让我们安装 Jasmine 和 Jasmine-node(Jasmine 和我们的 Node.js 环境的集成)
现在让我们安装两个我们的测试套件将使用的库。它将使用 Parse SDK 连接到一个虚假的 Parse 服务器,并使用 events 库来模拟请求对象
现在,使用 Jasmine 工具,让我们初始化我们的测试目录。
如果你愿意,可以全局安装 jasmine,使用 $ npm install -g jasmine, 然后你可以用这个初始化 $ jasmine init
本指南将假设你没有全局安装 Jasmine,尽管推荐这样做。如果你这样做,你可以将所有‘/node_modules/jasmine/bin/jasmine.js’的实例替换为‘jasmine’
这应该会创建一个名为 spec 的目录,其中包含一个支持文件夹,包含 Jasmine 的配置信息。
默认情况下,Jasmine 知道查找以“.spec.js”扩展名结尾的文件,因此我们将相应地命名我们的测试。
为我们的第一个单元测试创建文件:
添加一个实用程序目录,里面有两个将帮助我们测试的文件:
最后,在同一目录中创建一个常量文件。这个文件的实用程序将在后面解释
现在你的目录应该是这样的:
4. 替换为测试 Parse 服务器
围绕 Parse 进行测试
由于我们的方法涉及 Parse 服务器,我们希望能够测试这种交互。有两种方法可以做到这一点:
A. 我们可以通过定义一个实现相同接口的对象来“存根”Parse SDK对象。然后简单地将该对象作为参数传递给我们的云方法。那可能看起来像这样。
1 var ParseStub = {
2 // make sure all used methods and properties are defined
3 User : function () {
4 // Constructor function
5 this.set = function (key, val) {
6 // logic here to implement the parse object set
7 }
8 }
9 }
10 signupUser(ParseStub); // returns cloud function that we can testB. 另一种方法是设置一个真实的Parse服务器,仅用于测试数据。这将涉及Parse使用的慢HTTP层,但也允许我们测试数据库中的数据。在我们的测试中,我们需要导入Parse SDK,并将其配置为测试服务器。

在测试云代码时可以存根的两个地方:A.) 存根一个不会发出HTTP请求的Parse SDK,或B.) 交换一个测试数据库实现。
这两种方法都不是“正确”的答案。这取决于你想测试什么。存根Parse SDK的接口(即使只是我们使用的部分)是很多工作。此外,在这个例子中,我们将测试保存后的数据持久性,因此我们将使用第二种方法。
让我们:
- 在Back4App上创建一个测试Parse服务器
- 获取应用程序ID和主密钥,并将其保存到我们的常量文件中
- 在我们的规范文件中初始化Parse SDK,以便我们的测试使用测试服务器
欢迎您在本地运行一个 Parse服务器进行测试。我们只需在我们的仪表板中创建另一个Back4App应用程序。
如果您需要关于如何配置另一个Back4App服务器的复习,请前往 创建新应用程序教程。可以随意命名您的应用程序,尽管使用类似TestBackend的名称可能更明智。然后只需从仪表板 > 应用设置 > 安全性和密钥中获取应用程序ID和主密钥。
现在将这些令牌保存在我们的常量文件中,如下所示:
1 // ./spec/constants.js
2 // Paste in your app id and master key where the strings are
3 module.exports = {
4 APPLICATION_ID : "PASTE YOUR APPLICATION KEY HERE",
5 MASTER_KEY : "PASTE YOUR MASTER KEY HERE"
6 }请勿使用您生产应用程序的应用程序 ID 和主密钥!!!我们将删除数据,这样做将有丢失数据的风险。
5. 测试工具
云函数作为参数传递给 Express 请求和响应对象。
服务器在云上运行时会自动创建这些参数,因此对于我们的测试环境,我们必须创建双重参数。
这个案例更简单。当调用云函数时,会传递数据;在我们的案例中,传递的是个人资料和用户信息。提供的每个参数都可以从 request.params 属性中访问。
所以如果我们调用一个云函数,如下所示
1 // client code, calling Parse function
2 Parse.Cloud.run('fakefunction',
3 {
4 data1: 'I am data1',
5 data2: {
6 prop : "Nested property"
7 }
8 }
9 );那么 request.params 属性将包含传递的数据:
1 // Server code, running the Parse function
2 console.log(request.params);
3 // {
4 // data1: 'I am data1',
5 // data2: {
6 // prop : "Nested property"
7 // }
8 // }对于我们的测试来说,调用云函数时第一个参数应该是以下形式
1 {
2 params : {
3 username: 'testuser',
4 firstname: "John",
5 // the rest of the arguments
6 }
7 }因此在这种情况下我们不需要创建一个特殊的模拟对象。
响应对象允许云代码向客户端发送HTTP响应,表示成功或失败。我们想知道在调用云函数时这被称为什么。下面是一个 模拟对象,这将允许我们的测试确定调用是否成功。如果这让你感到困惑,不用担心,只需将其放入你的 ./spec/utils/response-stub.js 文件中。
1 // ./spec/utils/response-stub.js
2 const EventEmitter = require('events');
3 /**
4 * Wrapper around response stub. Simplifies testing cloud functions that
5 * employ a response parameter
6 */
7 function ResponseStub () {
8 this.responseListener = new EventEmitter();
9 this.responseStub = {};
10 /**
11 * Success method that cloud functions expect
12 */
13 this.responseStub.success = (resp) => {
14 this.responseListener.emit("success", resp);
15 }
16 /**
17 * Error method that cloud functions expect
18 */
19 this.responseStub.error = (resp) => {
20 this.responseListener.emit("error", resp);
21 }
22 /**
23 * Listens for errors and successes from stub and returns Promise that resolves or rejects accordingly
24 */
25 this.resolver = new Promise((resolve, reject) => {
26 this.responseListener.on("success", (resp) => resolve(resp));
27 this.responseListener.on("error", (err) => reject(err));
28 });
29 }
30
31 /**
32 * reeturns stub to feed to cloud function
33 */
34 ResponseStub.prototype.getStub = function () {
35 return this.responseStub;
36 }
37
38 /**
39 * returns promise that will indicate the success or failure
40 */
41 ResponseStub.prototype.onComplete = function () {
42 return this.resolver;
43 }
44
45 module.exports = ResponseStub;简而言之,这个javascript构造函数将为我们的测试提供一种传入响应对象的方法,该对象通过Promise的解析/拒绝指示云函数是否会返回成功或错误。
清理数据库
显然,我们不希望我们的测试Parse数据库保留在测试期间积累的内容。让我们定义一个用于清除数据库表的工具,可以在测试用例之前(或之后)调用。
将以下内容添加到‘spec/utils/purge-parse-table.js’:
1 // spec/utils/purge-parse-table.js
2 /**
3 * Removes all rows from the Parse Database
4 * @param {string} tablename the name of the parse table to be purged
5 * @return {Promise} promise to destroy each item in the table
6 */
7 module.exports = function (Parse) {
8 return (tablename) => {
9 var tableQuery;
10 if (tablename === "User")
11 tableQuery = new Parse.Query(Parse.User);
12 else tableQuery = new Parse.Query(tablename);
13 return tableQuery.find({useMasterKey : true}).then((items) => {
14 var destroyQueue = [];
15 for (var i=0; i<items.length; i++) {
16 destroyQueue.push(items[i].destroy({useMasterKey : true}));
17 }
18 return Promise.all(destroyQueue).catch((e) => {console.log("Error destroying: " + e.message)});
19 });
20 }
21 }在定义此函数后,提醒您确保您的spec/utils/constants.js已配置为您的测试解析应用程序,而不是您的生产解析应用程序。这将删除数据,因此请确认这是您上面创建的空数据库。
此函数接受我们配置的解析SDK,并返回另一个函数。返回的函数接受一个表名,并从相应的解析表中删除所有数据。
再次强调,返回一个函数的想法可能看起来很奇怪,但它允许测试规范配置解析端点,然后引用一个将清除该解析端点表的函数。
太棒了!现在让我们编写我们的测试!
6. 测试云函数如果未传递正确的参数将发送错误
云函数依赖于某些参数的包含,应该在例如未发送‘firstname’时失败。让我们确保。
我们将编辑我们的测试文件(终于!)spec/signup-user.spec.js。
在测试定义之前需要发生以下事情:
- 导入 Parse NodeJS SDK
- 导入我们的常量,并配置 Parse SDK 指向我们的测试服务器
- 导入我们的云函数
- 导入我们的“清除表”工具
- 导入我们创建的响应模拟对象
以下内容将会完成:
1 // Hook into your testing server
2 var Parse = require('parse/node');
3 var constants = require("./constants");
4 // head over to your Parse dash board for your test server, and grab your keys. Swap out the strings with the place holders below
5 Parse.initialize(constants.APPLICATION_KEY, null, constants.MASTER_KEY);
6 // if you are running a localhost Parse server, set the serverUrl accordingly
7 Parse.serverURL = 'https://parseapi.back4app.com'
8 var signupUser = require("../cloud/cloud-functions").SignupUser(Parse);
9 var purgeTable = require("./utils/purge-parse-table")(Parse);
10 var ResponseStub = require("./utils/response-stub");现在让我们添加测试用例。Jasmine 介绍可能有助于更好地理解结构,但它看起来是这样的(摘自介绍):
因此,describe 块封装测试套件,而 ‘it’ 块表示案例和期望。
通过向 ‘it’ 块传递参数,你可以异步运行测试。测试不会完成,直到参数被调用,如下所示:
这很有帮助,因为我们的一个测试将使用 HTTP,因此应该以这种方式异步运行,因为在 NodeJS 中使用 HTTP 是一个非阻塞过程。
此外,Jasmine 允许在测试套件中使用特殊块,这些块可以在测试生命周期的不同点运行。我们希望在每个测试之前删除所有表,因此我们将在 beforeEach 块中执行清除代码。
够了,开始添加一些代码吧!将下面的代码放入你的 spec/signup-user.spec.js 文件中,放在我们已经添加的导入下面:
1 //spec/signup-user.spec.js
2 // imports above
3 describe("SignupUser", ()=> {
4 beforeEach((done) => {
5 /// purge the user and profile tables, and then proceed
6 Promise.all([purgeTable("User"), purgeTable("Profile")])
7 .catch((e) => fail(e))
8 .then(() => done());
9 });
10 it ("should reject a request to signup that does not contain all the parameters", (done) => {
11 var responseStub = new ResponseStub();
12 responseStub.onComplete()
13 .then(() => fail("Should have failed due to invalid parameters"))
14 .catch((e) => {})
15 .then(() => done());
16
17 signupUser({ params : {}}, responseStub.getStub());
18
19 });
20 });太棒了,我们的第一个测试完成了。在 beforeEach 块中,我们清除了用户和个人资料表。然后触发第一个测试用例。它验证将无效参数传递给 signupUser 函数会导致该函数发送错误。
它使用响应存根来确保函数最终被拒绝。因为 ‘signupUser’ 会失败,所以存根上的初始 ‘then’ 块不应该被调用。如果被调用了,那么我们的测试就失败了!
继续使用以下命令运行测试:
你应该看到以下输出:
7. 数据持久性测试
希望你还有一次测试的能力!我们将验证当我们的云函数正常运行时,我们的数据库将如预期那样:将存在一个个人资料,并且有一个对用户对象的引用,二者都具有预期的属性。
将以下代码块添加到我们现有的‘describe’套件块中:
1 //spec/signup-user.spec.js
2 // inside describe
3 it ("should signup a User, and also create a Profile that contains a reference to the user", (done) => {
4 var responseStub = new ResponseStub();
5 var stub = responseStub.getStub();
6 signupUser({
7 params : {
8 firstname : "John",
9 lastname : "Smith",
10 email : "[email protected]",
11 username : "jsmith1",
12 password : "SecretCatchphrase1"
13 },
14 },
15 stub
16 );
17 responseStub.onComplete()
18 .then((resp) => {
19 var profileQ = new Parse.Query("Profile");
20 profileQ.equalTo("lastname", "Smith");
21 return profileQ.find({useMasterKey : true});
22 })
23 // Check to make sure the profile we retrieve is valid
24 .then((profiles) => {
25 if (profiles.length === 0) throw new Error("No profile's found");
26 expect(profiles[0].get('firstname')).toBe("John");
27 // get the corresponding user
28 return profiles[0].get("user").fetch({useMasterKey : true})
29 })
30 // Check to make sure the user is what we expect
31 .then((user) => {
32 expect(user.getUsername()).toBe("jsmith1");
33 })
34 .catch((e) => {
35 console.log(e)
36 fail(e);
37 })
38 .then(() => done());
39 });好的,这很多内容,所以让我们逐步了解发生了什么。
我们实例化一个响应模拟对象,如第一个测试用例中所示。然后我们使用包含 有效 参数的请求双重运行signupUser,以及响应模拟(第6-16行)。
接下来,这段代码监听模拟对象的onComplete方法,该方法将返回一个Promise。如果调用了response.error,Promise将被拒绝;如果调用了response.success,Promise将被解析。任何拒绝将导致Promise链跳过到catch块。因此,fail方法被放置在catch块中,因为如果Promise拒绝,测试应该失败。
Promise的响应 应该解析为个人资料对象。一旦解析,我们将查询与我们创建的相同姓氏的个人资料(第19-21行)。然后测试确认个人资料的‘firstname’与我们传递的相同(第25-26行)。
下一个代码块获取与个人资料关联的用户对象。Parse对象指针单独获取,因此需要另一个Promise块。
最后,代码确认对应的用户具有传递给signupUser函数的用户名。然后测试结束。
继续再运行一次测试套件: 继续使用以下命令运行测试:
你应该会看到以下输出:
太棒了!我们编写了一些云代码,并集成了测试框架。
结论
如果你有任何困惑,或者只是想要这个示例的代码,请前往 GitHub 仓库。按照说明下载并运行。
如果有什么不清楚的,或者不工作,请通过我的 Gmail 联系我,jackconsidine3。
我希望你喜欢这个教程,并获得了一些见解!