设为首页 收藏本站
查看: 1037|回复: 0

MEAN实践——LAMP的新时代替代方案(下)

[复制链接]

尚未签到

发表于 2015-8-19 15:14:33 | 显示全部楼层 |阅读模式
  在本系列文章的第一部分旨在介绍一些应用程序的基础技术细节和如何进行数据建模,而这个部分文章将着手建立验证应用程序行为的测试,并会指出如何启动和运行应用程序。

首先,编写测试
  首先定义一些小型配置库。文件名:test/config/test_config.js
  

module.exports = {  url : 'http://localhost:8000/api/v1.0'
  
}
  

  服务器运行端口是 localhost 8000 ,对于初始的测试来说非常适合。之后,如果改变产品系统的位置或者端口号,只需要简单地修改这个文件就可以。为了良好地测试,首先应该建立 1 个好的测试环境,这点可以通过下面的代码保证。首先,连接到数据库。
  文件名: est/setup_tests.js 。
  

function connectDB(callback) {  mongoClient.connect(dbConfig.testDBURL, function(err, db) {
  assert.equal(null, err);
  reader_test_db = db;
  console.log("Connected correctly to server");
  callback(0);
  });
  
}
  

  下一步,drop user collection,这么做可以了解数据库状态。
  

function dropUserCollection(callback) {  console.log("dropUserCollection");
  user = reader_test_db.collection('user');
  if (undefined != user) {
  user.drop(function(err, reply) {
  console.log('user collection dropped');
  callback(0);
  });
  } else {
  callback(0);
  }
  },
  

  下一步,drop user feed entry collection。
  

function dropUserFeedEntryCollection(callback) {  console.log("dropUserFeedEntryCollection");
  user_feed_entry = reader_test_db.collection('user_feed_entry');
  if (undefined != user_feed_entry) {
  user_feed_entry.drop(function(err, reply) {
  console.log('user_feed_entry collection dropped');
  callback(0);
  });
  } else {
  callback(0);
  }
  
}
  

  下一步,连接到Stormpath,随后删点所有测试应用程序中的用户。
  

function getApplication(callback) {  console.log("getApplication");
  client.getApplications({
  name: SP_APP_NAME
  }, function(err, applications) {
  console.log(applications);
  if (err) {
  log("Error in getApplications");
  throw err;
  }
  app = applications.items[0];
  callback(0);
  });
  
},
  
function deleteTestAccounts(callback) {
  app.getAccounts({
  email: TU_EMAIL_REGEX
  }, function(err, accounts) {
  if (err) throw err;
  accounts.items.forEach(function deleteAccount(account) {
  account.delete(function deleteError(err) {
  if (err) throw err;
  });
  });
  callback(0);
  });
  
}
  

  下一步,关闭数据库。
  

function closeDB(callback) {  reader_test_db.close();
  
}
  

  最终,调用 async.series 来保证所有函数都按次序运行。
  

async.series([connectDB, dropUserCollection,    dropUserFeedEntryCollection, dropUserFeedEntryCollection, getApplication, deleteTestAccounts, closeDB]);  

  Frisby 在初期就被建立,这里将使用它定义测试用例,如下:
  文件名:test/createaccountserror_spec.js
  

TU1_FN = "Test";  
TU1_LN = "User1";
  
TU1_EMAIL = "testuser1@example.com";
  
TU1_PW = "testUser123";
  
TU_EMAIL_REGEX = 'testuser*';
  
SP_APP_NAME = 'Reader Test';
  

  
var frisby = require('frisby');
  
var tc = require('./config/test_config');
  

  下面代码将从 enroll route 开始。这个用例故意丢掉了 first name 字段,因此获得 1 个 400 与 1 个 JSON error(显示 first name 未定义)返回,下面就 toss that frisby:
  

frisby.create('POST missing firstName')  
.post(tc.url + '/user/enroll',
  { 'lastName' : TU1_LN,
  'email' : TU1_EMAIL,
  'password' : TU1_PW })
  
.expectStatus(400)
  
.expectHeader('Content-Type', 'application/json; ')
  
.expectJSON({'error' : 'Undefined First Name'})
  
.toss()
  

  下面用例将测试不包含小写字母,这同样会导致 Stormpath 返回错误,以及返回400 状态。
DSC0000.jpg
  下面将测试一个无效邮箱地址。因此,期望返回的是未发现 @ 标志,以及 emali地址缺少域名,同时也会获得 1 个 400 状态。
  文件名:test/createaccountsspec.js
  

frisby.create('POST invalid email address')  
.post(tc.url + '/user/enroll',
  { 'firstName' : TU1_FN,
  'lastName' : TU1_LN,
  'email' : "invalid.email",
  'password' : 'testUser' })
  
.expectStatus(400)
  
.expectHeader('Content-Type', 'application/json; charset=utf-8')
  
.expectJSONTypes({'error' : String})
  
.toss()
  

  下面着眼一些可以运行的例子,首先需要定义 3 个用户。
  文件名:test/createaccountsspec.js
  

TEST_USERS = [{'fn' : 'Test', 'ln' : 'User1',  'email' : 'testuser1@example.com', 'pwd' : 'testUser123'},
  {'fn' : 'Test', 'ln' : 'User2',
  'email' : 'testuser2@example.com', 'pwd' : 'testUser123'},
  {'fn' : 'Test', 'ln' : 'User3',
  'email' : 'testuser3@example.com', 'pwd' : 'testUser123'}]
  

  
SP_APP_NAME = 'Reader Test';
  

  
var frisby = require('frisby');
  
var tc = require('./config/test_config');
  

  下面用例将发送 1 个包含上文已定义 3 个用户的数组,当然期望获得代表成功的 201 状态。返回的 JSON document 将展示已建立的用户对象,因此这里可以检查测试数据匹配与否。
  

TEST_USERS.forEach(function createUser(user, index, array) {  frisby.create('POST enroll user ' + user.email)
  .post(tc.url + '/user/enroll',
  { 'firstName' : user.fn,
  'lastName' : user.ln,
  'email' : user.email,
  'password' : user.pwd })
  .expectStatus(201)
  .expectHeader('Content-Type', 'application/json; charset=utf-8')
  .expectJSON({ 'firstName' : user.fn,
  'lastName' : user.ln,
  'email' : user.email })
  .toss()
  
});
  

  下一步将测试重复用户。下例将验证这个用户注册的 email 地址已经被使用。
  

frisby.create('POST enroll duplicate user ')  .post(tc.url + '/user/enroll',
  { 'firstName' : TEST_USERS[0].fn,
  'lastName' : TEST_USERS[0].ln,
  'email' : TEST_USERS[0].email,
  'password' : TEST_USERS[0].pwd })
  
.expectStatus(400)
  
.expectHeader('Content-Type', 'application/json; charset=utf-8')
  
.expectJSON({'error' : 'Account with that email already exists.  Please choose another email.'})
  
.toss()
  

  这里存在一个重要问题,无法知道 Stormpath 会优先返回哪个 API key。因此,这里需要建立一个动态文件。随后可以使用这个对文件来验证测试用例——用户身份验证组件。
  文件名称: /tmp/readerTestCreds.js
  

TEST_USERS =  
[{"_id":"54ad6c3ae764de42070b27b1",
  
"email":"testuser1@example.com",
  
"firstName":"Test",
  
"lastName":"User1",
  
"sp_api_key_id":”",
  
"sp_api_key_secret":””
  
},
  
{"_id":"54ad6c3be764de42070b27b2”,
  "email":"testuser2@example.com",
  "firstName":"Test",
  "lastName":"User2”,
  "sp_api_key_id":”",
  "sp_api_key_secret":””
  
}];
  
module.exports = TEST_USERS;
  

  为了建立上面这个临时文件,这里需要连接 MongoDB 从而检索用户信息。代码如下:
  文件名:tests/writeCreds.js
  

TU_EMAIL_REGEX = new RegExp('^testuser*');  
SP_APP_NAME = 'Reader Test';
  
TEST_CREDS_TMP_FILE = '/tmp/readerTestCreds.js';
  

  
var async = require('async');
  
var dbConfig = require('./config/db.js');
  
var mongodb = require('mongodb');
  
assert = require('assert');
  

  
var mongoClient = mongodb.MongoClient
  
var reader_test_db = null;
  
var users_array = null;
  

  
function connectDB(callback) {
  

  mongoClient.connect(dbConfig.testDBURL, function(err, db) {
  assert.equal(null, err);
  reader_test_db = db;
  callback(null);
  });
  }
  

function lookupUserKeys(callback) {  

  console.log("lookupUserKeys");
  user_coll = reader_test_db.collection('user');
  user_coll.find({email : TU_EMAIL_REGEX}).toArray(function(err, users) {
  users_array = users;
  callback(null);
  });
  }
  

function writeCreds(callback) {  var fs = require('fs');
  

  fs.writeFileSync(TEST_CREDS_TMP_FILE, 'TEST_USERS = ');
  fs.appendFileSync(TEST_CREDS_TMP_FILE, JSON.stringify(users_array));
  fs.appendFileSync(TEST_CREDS_TMP_FILE, '; module.exports = TEST_USERS;');
  callback(0);
  }
  

function closeDB(callback) {  reader_test_db.close();
  }
  

  async.series([connectDB, lookupUserKeys, writeCreds, closeDB]);
  

  着眼下面代码,上文建立的临时文件在第一行就会被使用。同时,有多个 feeds 被建立,比如 Dilbert 和 the Eater Blog 。
  文件名:tests/feed_spec.js
  

TEST_USERS = require('/tmp/readerTestCreds.js');  

  
var frisby = require('frisby');
  
var tc = require('./config/test_config');
  
var async = require('async');
  
var dbConfig = require('./config/db.js');
  

  
var dilbertFeedURL = 'http://feeds.feedburner.com/DilbertDailyStrip';
  
var nycEaterFeedURL = 'http://feeds.feedburner.com/eater/nyc';
  

  首先,一些用户会被建立,当然他们并没有订阅任何 feeds。下面代码将测试 feeds 的订阅。请注意,这里同样需要进行身份验证,通过使用 .auth 和 Stormpath API keys 完成。
  

function addEmptyFeedListTest(callback) {  

  var user = TEST_USERS[0];
  frisby.create('GET empty feed list for user ' + user.email)
  .get(tc.url + '/feeds')
  .auth(user.sp_api_key_id, user.sp_api_key_secret)
  .expectStatus(200)
  .expectHeader('Content-Type', 'application/json; charset=utf-8')
  .expectJSON({feeds : []})
  .toss()
  callback(null);
  }
  下面用例将为第一个测试用户订阅 Dilbert feed 。
DSC0001.jpg
  这个用例将尝试为用户 feed 重复订阅。
  

function subDuplicateFeed(callback) {  

  var user = TEST_USERS[0];
  frisby.create('PUT Add duplicate feed sub for user ' + user.email)
  .put(tc.url + '/feeds/subscribe',
  {'feedURL' : dilbertFeedURL})
  .auth(user.sp_api_key_id, user.sp_api_key_secret)
  .expectStatus(201)
  .expectHeader('Content-Type', 'application/json; charset=utf-8')
  .expectJSONLength('user.subs', 1)
  .toss()
  callback(null);
  }
  下一步,将为测试用户添加一个新的 feed,返回的结果应该是用户当下已经订阅了 2 个 feed。
  

function subSecondFeed(callback) {  

  var user = TEST_USERS[0];
  frisby.create('PUT Add second feed sub for user ' + user.email)
  .put(tc.url + '/feeds/subscribe',
  {'feedURL' : nycEaterFeedURL})
  .auth(user.sp_api_key_id, user.sp_api_key_secret)
  .expectStatus(201)
  .expectHeader('Content-Type', 'application/json; charset=utf-8')
  .expectJSONLength('user.subs', 2)
  .toss()
  callback(null);
  }
  下一步,将使用第 2 个测试用户来订阅 1 个 feed 。
  

function subOneFeedSecondUser(callback) {  

  var user = TEST_USERS[1];
  frisby.create('PUT Add one feed sub for second user ' + user.email)
  .put(tc.url + '/feeds/subscribe',
  {'feedURL' : nycEaterFeedURL})
  .auth(user.sp_api_key_id, user.sp_api_key_secret)
  .expectStatus(201)
  .expectHeader('Content-Type', 'application/json; charset=utf-8')
  .expectJSONLength('user.subs', 1)
  .toss()
  callback(null);
  }
  

async.series([addEmptyFeedListTest, subOneFeed, subDuplicateFeed, subSecondFeed, subOneFeedSecondUser]);  

REST API
  在开始编写 REST API 代码之前,首先需要定义一些实用工具库。首先,需求定义应用程序如何连接到数据库。将这个信息写入一个独立的文件允许应用程序灵活地添加新数据库 URL,以应对开发或者生产系统。
  文件名:config/db.js
DSC0002.jpg
  如果期望打开数据库验证,这里需要将信息存入 1 个文件,如下文代码所示。出于多个原因,这个文件不应该被置入源代码控制。
  文件名称:config/security.js
  

module.exports = {  stormpath_secret_key : ‘YOUR STORMPATH APPLICATION KEY’;
  
}
  

  Stormpath API 和 Secret keys 应该被保存到属性文件,如下文代码所示,同事还需要严加注意。
  文件名:config/stormpath_apikey.properties
  

apiKey.id = YOUR STORMPATH API KEY>
apiKey.secret = YOUR STORMPATH API KEY SECRET  

Express.js 简述
  在 Express.js 中会建立应用程序(APP)。这个应用程序会监听制定的端口来响应 HTTP 请求。当请求涌入,它们会被传输到 1 个中间件链。中间件链中的每个 link 都会被给予 1 个请求和 1 个响应对象用以存储结果。link 分为两种类型,工作或者传递到下一个 link 。这里会通过 app.use() 来添加新的中间件。主中间件被称为「router(路由器)」,它会监听 URL,并将 URL/ 动作传递到 1 个指定的处理函数。

建立应用程序
  现在开始聚焦应用程序代码,鉴于可以在独立文件中为不同的 routes 嵌入处理器,所以应用程序的体积非常小。
  文件名:server.js
DSC0003.jpg
  在 chain 中末尾定义中间件来处理坏 URLs。
DSC0004.jpg
  现在,应用程序就会监听 8000 端口。
DSC0005.jpg
  在控制台将消息打印给用户。
  

console.log('Magic happens on port ' + port);  

  
exports = module.exports = app;
  

定义 Mongoose 数据模型
  这里会使用 Mongoose 将 Node.js 上的对象映射成 MongoDB 文档。如上文所述,这里将建立 4 个 collections:


  • Feed collection。  

  • Feed entry collection。  

  • User collection。  

  • User feed-entry-mapping collection。
  下一步,将为 4 个 collections 定义 schema。首先,从 user schema 开始。注意,这里同样可以格式化数据,比如讲字母都转换成小写,使用 trim 消除首/末空格。
  文件名:app/routes.js
  

var userSchema = new mongoose.Schema({  active: Boolean,
  email: { type: String, trim: true, lowercase: true },
  firstName: { type: String, trim: true },
  lastName: { type: String, trim: true },
  sp_api_key_id: { type: String, trim: true },
  sp_api_key_secret: { type: String, trim: true },
  subs: { type: [mongoose.Schema.Types.ObjectId], default: [] },
  created: { type: Date, default: Date.now },
  lastLogin: { type: Date, default: Date.now },
  },
  { collection: 'user' }
  
);
  

  下面代码将告诉 Mongoose 需要哪些索引。当索引不存在于 MongoDB 数据库中时,Mongoose 将会负责索引的建立。唯一性约束保障将去除重复出现的可能。「email : 1」 将以升序的方式维护地址,而「email : -1」则是降序。
  在其他 3 个 collections 上重复这个步骤。
  

var UserModel = mongoose.model( 'User', userSchema );  

  
var feedSchema = new mongoose.Schema({
  feedURL: { type: String, trim:true },
  link: { type: String, trim:true },
  description: { type: String, trim:true },
  state: { type: String, trim:true, lowercase:true, default: 'new' },
  createdDate: { type: Date, default: Date.now },
  modifiedDate: { type: Date, default: Date.now },
  },
  { collection: 'feed' }
  
);
  

  
feedSchema.index({feedURL : 1}, {unique:true});
  
feedSchema.index({link : 1}, {unique:true, sparse:true});
  

  
var FeedModel = mongoose.model( 'Feed', feedSchema );
  

  
var feedEntrySchema = new mongoose.Schema({
  description: { type: String, trim:true },
  title: { type: String, trim:true },
  summary: { type: String, trim:true },
  entryID: { type: String, trim:true },
  publishedDate: { type: Date },
  link: { type: String, trim:true  },
  feedID: { type: mongoose.Schema.Types.ObjectId },
  state: { type: String, trim:true, lowercase:true, default: 'new' },
  created: { type: Date, default: Date.now },
  },
  { collection: 'feedEntry' }
  
);
  

  
feedEntrySchema.index({entryID : 1});
  
feedEntrySchema.index({feedID : 1});
  

  
var FeedEntryModel = mongoose.model( 'FeedEntry', feedEntrySchema     );
  

  
var userFeedEntrySchema = new mongoose.Schema({
  userID: { type: mongoose.Schema.Types.ObjectId },
  feedEntryID: { type: mongoose.Schema.Types.ObjectId },
  feedID: { type: mongoose.Schema.Types.ObjectId },
  read : { type: Boolean, default: false },
  },
  { collection: 'userFeedEntry' }
  );
  

  下面是复合索引实例,每个索引都以升序维护。
  

userFeedEntrySchema.index({userID : 1, feedID : 1, feedEntryID : 1, read : 1});  

  
var UserFeedEntryModel = mongoose.model('UserFeedEntry', userFeedEntrySchema );
  

  每个用于 GET、POST、PUT 和 DELETE 的请求需要拥有 1 个正确的内容类型,也就是 application/json。然后下一个 link 会被调用。
DSC0006.jpg
  下一步需要为每个 URL/verb 定义处理器。参考资料部分附上了所有代码,下面只是代码片段。在这些代码中,Stormpath 带来的便捷一览无余。此外,这里定义的是 /api/v1.0 ,举个例子,这里客户端可以调用的是 /api/v1.0/user/enroll。如果使用 /api/v2.0,/api/v2.0 则可以被使用,当然向下兼容。
DSC0007.jpg

启动服务器并运行测试
  要启动服务器和运行测试,这里需要遵循几个步骤。


  •   保证 MongoDB 实例运行,mongod。

  •   安装 Node 库,npm install。

  •   开启 REST API 服务器,node server.js。

  •   运行测试用例:node setup_tests.js;jasmine-node create_accounts_error_spec.js;jasmine-node create_accounts_spec.js;node write_creds.js;jasmine-node feed_spec.js

  原文链接:Building your first application with MongoDB: Creating a REST API using the MEAN Stack - Part 2

参考文献:


  • HTTP status code definitions  

  • Chad Tindel’s Github Repository  

  • M101JS: MongoDB for Node.js Developers  

  • Data Models  

  • Data Modeling Considerations for MongoDB Applications
  本文系 OneAPM 工程师编译整理。想阅读更多技术文章,请访问 OneAPM 官方博客。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-101264-1-1.html 上篇帖子: Centos6.5 安装lamp环境 下篇帖子: Ubuntu下配置LAMP + PhpStorm
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表