NodeJS 秘籍(五)
十、连接到数据存储
如果您正在用 Node.js 构建一个应用,您几乎不可避免地需要某种形式的数据存储。这可以是简单的内存存储或任何数量的数据存储解决方案。Node.js 社区为您在应用开发中可能遇到的几乎所有数据存储创建了许多驱动程序和连接桥。在本章中,您将研究如何使用 Node.js 来连接其中的许多组件,包括:
- 关系型数据库
- 搜寻配置不当的
- 一种数据库系统
- MongoDB
- 数据库
- 使用心得
- 卡桑德拉
本章重点介绍 Node.js 与这些数据库的通信,而不是每个数据库的安装和初始化。因为本书旨在关注 Node.js 以及它如何在各种用例以及所有这些数据库类型中运行,所以鼓励您找到适合您特定数据库需求的方法。
10-1.连接到 MySQL
问题
许多开发人员最初是通过 MySQL 接触数据库编程的。正因为如此,许多人也希望将这种熟悉感(或桥梁)从现有应用带到 Node.js 应用中。因此,您希望能够从 Node.js 代码中连接到 MySQL 数据库。
解决办法
当您开始将 MySQL 集成到您的应用中时,您必须决定您希望用作 MySQL 驱动程序的 Node.js 框架。如果您选择使用 npm 并搜索 mysql,您可能会看到 MySQL 包列在顶部或顶部附近。然后用$ npm install mysql 命令安装这个包。
一旦安装了这个包,就可以在 Node.js 应用中使用 MySQL 了。为了连接和执行查询,你可以使用一个类似于你在清单 10-1 中看到的模块。在这个例子中,您可以利用 MySQL 示例数据库 Sakila,您可以根据在http://dev.mysql.com/doc/sakila/en/sakila-installation.html找到的说明来安装它。
清单 10-1 。连接和查询 MySQL
/**
* mysql
*/
var mysql = require('mysql');
var connectionConfig = {
host: 'localhost',
user: 'root',
password: '',
database: 'sakila'
};
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) {
console.log('connection::connected');
});
connection.query('SELECT * FROM actor', function(err, rows, fields) {
if (err) throw err;
rows.forEach(function(row) {
console.log(row.first_name, row.last_name);
});
});
var actor = { first_name: 'Wil', last_name: 'Wheaton' };
connection.query('INSERT INTO actor SET ?', actor, function(err, results) {
if (err) throw err;
console.log(results);
});
connection.end(function(err) {
console.log('connection::end');
});
它是如何工作的
使用 mysql 模块连接到 MySQL 从一个连接配置对象开始。您的解决方案中的连接对象只是提供您希望连接的主机、用户、密码和数据库。这些是基本的设置,但是还有其他选项可以在该对象上配置,如您在表 10-1 中所见。
表 10-1 。MySQL 的连接选项
| [计]选项 | 描述 |
|---|---|
| bigNumberStrings | 当与 supportBigNumbers 一起使用时,大数字将由 JavaScript 中的字符串表示。默认值:False |
| 字符集 | 命名要用于连接的字符集。默认值:UTF8_GENERAL_CI |
| 数据库ˌ资料库 | 列出 MySQL 服务器上的数据库名称。 |
| 调试 | 使用 stdout 打印详细信息。默认值:False |
| 旗帜 | 列出要使用的非默认连接标志。 |
| 圣体 | 提供您要连接的数据库服务器的主机名。默认值:本地主机 |
| 安全认证 | 允许连接到不安全的(旧的)服务器验证方法。默认值:False |
| 多重陈述 | 允许每个查询有多个语句。这可能会导致 SQL 注入的袭击。默认值:False |
| 密码 | 列出 MySQL 用户的密码。 |
| 港口 | 给出 MySQL 服务器实例所在机器的端口号。默认值:3306 |
| 查询格式 | 创建自定义查询函数。 |
| 套接字路径 | 提供 Unix 套接字的路径。这将导致主机和端口被忽略。 |
| stringifyObjects | 将字符串化对象,而不是转换它们的值。默认值:False |
| 支持 BigNumbers | 在列中使用 BIGINT 或 DECIMAL 时使用此选项。默认值:False |
| 时区 | 列出本地日期的时区。默认值:本地 |
| 分配担任特定类型角色 | 将类型转换为本机 JavaScript 类型。默认值:真 |
| 用户 | 列出用于身份验证的 MySQL 用户。 |
一旦创建了连接对象,就可以实例化一个到 MySQL 服务器的新连接。这是通过调用mysql.createConnection(config)来完成的,然后它将实例化连接对象并向其传递ConnectionConfig()对象。
你可以在清单 10-2 中看到,连接对象将实际尝试在协议模块中创建连接,该模块执行必要的 MySQL 握手以连接到服务器。
清单 10-2 。在 MySQL 模块中连接
module.exports = Connection;
Util.inherits(Connection, EventEmitter);
function Connection(options) {
EventEmitter.call(this);
this.config = options.config;
this._socket = options.socket;
this._protocol = new Protocol({config: this.config, connection: this});
this._connectCalled = false;
this.state = "disconnected";
}
现在您已经连接到 MySQL 服务器,您可以使用该连接进行查询。在该解决方案中,您能够执行两种不同类型的查询。
第一个查询是来自数据库中 actor 表的显式 select 语句。这只需要将查询正确地形成为connection.query方法的第一个参数的字符串。connection.query方法最多可以接受三个参数:sql、值和回调。如果 values 参数不存在,则通过检查它是否是一个函数来检测它,然后只有 SQL 被排队以在服务器上执行。一旦查询完成,回调将被返回。
在第二个查询中,传递一些您希望在数据库中设置的值。这些值是 JavaScript 对象的形式,它们被传递给“?”插入查询上的占位符。使用这种方法的一个好处是,mysql 模块会尝试安全地对您加载到数据库中的所有数据进行转义。它这样做是为了减轻 SQL 注入的攻击。mysql 模块中有一个转义矩阵,会对不同的值类型执行不同类型的转义(见表 10-2 )。
表 10-2 。逃离矩阵
| 值类型 | 它是如何转换的 |
|---|---|
| 数组 | 转向列表['a ',' b'] => 'a ',' b ' |
| 布尔代数学体系的 | True' / 'false '字符串 |
| 缓冲 | 十六进制字符串 |
| 日期 | ' YYYY-mm-dd HH:ii:ss '字符串 |
| NaN/无穷大 | 因为 MySQL 没有将它们转换成 |
| 嵌套数组 | 分组列表[['a ',' b'],['c ',' d']] => ('a ',b '),(' c ',' d ') |
| 民数记 | 没有人 |
| 目标 | 生成键-'值'对;嵌套对象变成字符串 |
| 用线串 | 安全逃脱 |
| 未定义/空 | 空 |
这只是一个使用 mysql 模块连接和执行 MySQL 查询的基本示例。你也可以使用其他方法。您可以对查询的响应进行流式处理,并绑定到事件,以便在返回某一行时对该行执行特定的操作,然后再继续处理下一行。这方面的一个例子如清单 10-3 所示。
清单 10-3 。流式传输一个查询
/**
* mysql
*/
var mysql = require('mysql');
var connectionConfig = {
host: 'localhost',
user: 'root',
password: '’,
database: 'sakila'
};
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) {
console.log('connection::connected');
});
var query = connection.query('SELECT * FROM actor');
query.on('error', function(err) {
console.log(err);
}).on('fields', function(fields) {
console.log(fields);
}).on('result', function(row) {
connection.pause();
console.log(row);
connection.resume();
}).on('end', function(err) {
console.log('connection::end');
});
在这里,您可以看到查询本身并没有改变;我们不是向查询方法传递回调,而是绑定到查询执行时发出的事件。因此,在解析字段时,会对它们进行处理。然后,对于每一行,在移动到下一条记录之前,处理该数据。这是通过使用 connection.pause()函数,然后执行您的动作,接着是connection.resume()方法来完成的。
当您使用 mysql 模块这样的框架时,在 Node.js 中连接和使用 MySQL 非常简单。如果 MySQL 是您选择的数据库,它不应该限制您选择 Node.js 作为数据访问服务器的能力。
10-2.连接到微软 SQL 服务器
问题
您希望将 Node.js 应用集成到 Microsoft SQL Server 实例中。
解决办法
就像 MySQL 一样,使用 Node.js 为 Microsoft SQL Server 寻找驱动程序有几种解决方案。其中最受欢迎的一个包是“乏味的”,以连接到 SQL Server 的表格数据流(TDS) 协议命名。您首先使用$ npm install tedious命令通过 npm 安装这个包。
然后,构建一组与 SQL Server 交互的模块。这个解决方案的第一部分,清单 10-4 ,利用 dravoid 创建一个到 SQL Server 实例的连接。第二部分,如清单 10-5 所示,是包含与 SQL Server 实例上的数据交互的模块。
注意 SQL Server 是微软的产品。因此,只有当您的服务器运行 Windows 和 SQL Server 时,以下实现才有效。
清单 10-4 。将连接到您的 SQL Server 实例
/*
* Using MS SQL
*/
var TDS = require('tedious'),
Conn = TDS.Connection,
aModel = require('./10-2-1.js');
var conn = new Conn({
username: 'sa',
password: 'pass',
server: 'localhost',
options: {
database: 'Northwind',
rowCollectionOnRequestCompletion: true
});
function handleResult(err, res) {
if (err) throw err;
console.log(res);
}
conn.on('connect', function(err) {
if (err) throw err;
aModel.getByParameter(conn, 'parameter', handleResult);
aModel.getByParameterSP(conn, 'parameter', handleResult);
});
清单 10-5 。正在查询微软 SQL 服务器
var TDS = require('tedious'),
TYPES = TDS.TYPES,
Request = TDS.Request;
var aModel = module.exports = {
// Use vanilla SQL
getByParameter: function(conn, parm, callback) {
var q = 'select * from model (NOLOCK) where identifier = @parm';
var req = new Request(q, function(err, rowcount, rows) {
callback( err, rows );
});
req.addParameter('parm', TYPES.UniqueIdentifierN, parm);
conn.execSql(req);
},
// Use a Store Procedure
getByParameterSP: function(conn, parm, callback) {
var q = 'exec sp_getModelByParameter @parm';
var req = new Request(q, function(err, rowcount, rows) {
callback( err, rows );
});
req.addParameter('parm', TYPES.UniqueIdentifierN, parm);
conn.execSql(req);
}
};
它是如何工作的
当您第一次使用繁琐的模块连接到 Microsoft SQL Server 时,首先需要创建一个连接。这是通过使用TDS.Connection对象并用配置对象实例化它来完成的。在您的解决方案中,要创建连接,您需要发送用户名、密码、服务器名和一组用于连接的选项。有许多选项可以传递给这个对象,如表 10-3 所示。
表 10-3 。TDS。连接配置
| 环境 | 描述 |
|---|---|
| 选项.取消超时 | 取消请求超时前的时间。默认值:5 秒 |
| 选项. connectTimeout | 等待连接尝试超时的时间。默认值:15 秒 |
| 选项.加密凭证详细信息 | 对象,该对象将包含加密所需的任何凭据。默认值:空对象“{ 0 }” |
| 选项.数据库 | 要连接的数据库的名称 |
| 选项.调试.数据 | 布尔值,表示是否发送关于数据包数据的调试信息。默认值:False |
| 选项. debug.packet | 布尔值,表示是否发送关于数据包的调试信息。默认值:False |
| 选项. debug.payload | 布尔值,表示是否发送有关数据包有效负载的调试信息。默认值:False |
| 选项. debug.token | 布尔值,表明是否发送有关流标记的调试信息。默认值:False |
| 选项.加密 | 设置是否加密请求。默认值:False |
| 选项。实例 Name | 要连接的命名实例。 |
| 选项。isolationLevel | 服务器上的隔离级别,或者服务器何时允许从另一个操作中看到数据。默认值:未提交读(这被称为“脏读”,或最低级别的隔离。给定的事务可以看到来自另一个事务的未提交的事务。) |
| 选项. packetSize | 发送到服务器和从服务器接收的数据包的大小限制。默认值:4 KB |
| 选项.端口 | 要连接的端口。此选项与 options.instanceName 互斥。默认值:1433 |
| 选项. requestTimeout | 给定请求超时前的时间。默认值:15 秒 |
| 选项. rowcollectionondone | 一个布尔值,表示当发出“done”、“doneInProc”和“doneProc”事件时将接收行集合。默认值:False |
| options . rowcollectiononrequestcompletion | 布尔值,为真时,将在请求回调中提供行集合。默认值:False |
| options.tdsVersion | 连接要使用的 TDS 协议的版本。默认值:7_2 |
| options.textsize | 为文本数据类型设置任何列的最大宽度。默认:2147483647 |
| 。密码 | 与用户名关联的密码 |
| 。计算机 Web 服务器 | 您希望连接的服务器的名称或 IP 地址 |
| 。用户名 | 用于连接到 MS SQL Server 实例的用户名(注意:不支持 Windows 身份验证连接。) |
一旦将这些选项传递给连接对象,即 Node.js EventEmitter ,就可以绑定到“连接”事件。有几种方法可以从连接中发出“连接”事件,如下所述:
- 成功的连接
- 登录失败
- 在
connectTimeout过去之后 - 在连接过程中出现套接字错误后
一旦成功连接到 SQL Server,就可以调用包含您的请求的模块。TDS.Request是一个EventEmitter,它允许您通过普通的 T-SQL 字符串或存储过程来执行 SQL。该请求还接受回调,要么直接调用回调,要么将结果应用到'requestCompleted'事件。
正如许多 SQL Server 实现一样,您可以将参数传递给希望执行的 SQL。在您的解决方案的两个示例中(一个是 SQL 文本,一个是存储过程),您传递了一个命名参数。这个命名参数通过使用Request.addParameter()方法被添加到请求中。addParameter()方法最多接受四个参数:名称、类型、值和一个选项对象。添加参数时使用的类型可以是允许作为参数一部分的TDS.Types对象中的任何类型。它们是 Bit、TinyInt、SmallInt、Int、BigInt、Float、Real、SmallDateTime、DateTime、VarChar、Text、NVarChar、Null、UniqueIdentifier 和 UniqueIdentifierN。
一旦创建了请求对象,并添加了所需的参数,就可以通过调用connection.execSql(<Request>)来执行 SQL 语句,将请求传递给方法。当请求完成时,您的回调执行,您可以相应地处理结果和行。
现在,您已经了解了如何使用 Node.js 和繁琐的包来管理 TDS 连接,从而实现到 MS SQL Server 的连接。
10-3.通过 Node.js 使用 PostgreSQL】
问题
您将在数据库中使用 PostgreSQL ,并且需要在 Node.js 应用中利用它。
解决办法
有几个软件包可用于连接 PostgreSQL。这个解决方案将利用 node-postgres 模块,这是 PostgreSQL 的一个低级实现。清单 10-6 显示了一个简单的例子,连接到一个 PostgreSQL 实例,执行一个简单的查询,然后记录结果。
清单 10-6 。连接到 PostgreSQL 并执行查询
/**
* PostgreSQL
*/
var pg = require('pg');
var connectionString = 'tcp://postgres:pass@localhost/postgres';
var client = new pg.Client(connectionString);
client.connect(function(err) {
if (err) throw err;
client.query('SELECT EXTRACT(CENTURY FROM TIMESTAMP "2011-11-11 11:11:11")', function(err, result) {
if (err) throw err;
console.log(result.rows[0]);
client.end();
});
});
它是如何工作的
这个解决方案从使用$ npm install pg安装 node-postgres 模块开始。然后可以将它添加到 Node.js 代码中。然后,通过实例化一个新的客户机来创建到 PostgreSQL 实例的连接。客户端构造器可以解析连接字符串参数,然后你可以创建一个连接,如清单 10-7 所示。
清单 10-7 。node-postgres 的客户端构造
var Client = function(config) {
EventEmitter.call(this);
this.connectionParameters = new ConnectionParameters(config);
this.user = this.connectionParameters.user;
this.database = this.connectionParameters.database;
this.port = this.connectionParameters.port;
this.host = this.connectionParameters.host;
this.password = this.connectionParameters.password;
var c = config || {};
this.connection = c.connection || new Connection({
stream: c.stream,
ssl: c.ssl
});
this.queryQueue = [];
this.binary = c.binary || defaults.binary;
this.encoding = 'utf8';
this.processID = null;
this.secretKey = null;
this.ssl = c.ssl || false;
};
一旦创建了这个连接,接下来就要执行一个查询。这是通过调用client.query()并传递一个 SQL 字符串作为第一个参数来完成的。第二个参数可以是应用于查询的一组值,就像你在 10-1 节看到的那样,也可以是回调函数。回调函数将传递两个参数、一个错误(如果存在)或查询结果。如您所见,结果将包含一个返回行的数组。一旦您处理了结果,您就可以通过调用client.end()来关闭客户端连接。那个。end()方法将通过connection.end()方法关闭连接。
您的示例使用明文 SQL 语句来执行 node-postgres。使用 node-postgres 执行查询还有另外两种方法:参数化和预处理语句。
参数化查询允许您向查询传递参数,例如'select description from products where name=$1', ['sandals']'。通过使用参数化查询,您可以针对 SQL 注入攻击提供更高级别的保护。它们的执行速度也比纯文本查询慢,因为在每次执行之前,这些语句都要准备好,然后再执行。
使用 node-postgres 可以执行的最后一种查询是预处理语句。其中一个将被准备一次,然后对于到 postgres 的每个会话连接,这个 SQL 查询的执行计划被缓存,这样如果它被执行多次,它将成为使用 node-postgres 执行 SQL 的最有效的方式。像参数化查询一样,预处理语句也为 SQL 注入攻击提供了类似的屏障。准备好的语句是通过将一个对象传递给具有名称、文本和 values 属性的查询方法来创建的。然后,您可以通过您为它们提供的名称来调用这些准备好的语句。
利用 node-postgres 允许您从 Node.js 应用中直接高效地与 PostgreSQL 进行交互。接下来的部分将脱离 Node.js 的传统 SQL 接口,您将开始研究几种用于连接 Node.js 的非 SQL 选项。
10-4.使用 Mongoose 连接到 MongoDB
问题
您希望能够在 Node.js 应用中利用 MongoDB 。为此,您选择与 Mongoose 集成。
解决办法
当您在 Node.js 应用中使用 MongoDB 时,有许多驱动程序可供您选择来连接到您的数据存储。然而,最广泛使用的解决方案可能是将您的 MongoDB 实例与 Mongoose 模块集成。用$ npm install mongoose安装后,您可以使用清单 10-8 中列出的连接方法创建一个到 MongoDB 的连接。
清单 10-8 。使用 Mongoose 连接到 MongoDB
/**
* Connecting to MongoDB with Mongoose
*/
var mongoose = require('mongoose');
// simple connection string
// mongoose.connect('mongodb://localhost/test');
mongoose.connect('mongodb://localhost/test', {
db: { native_parser: false },
server: { poolSize: 1 }
// replset: { rs_name : 'myReplicaSetName' },
// user: 'username',
// pass: 'password'
});
// using authentication
// mongoose.connect('mongodb://username:password@host/collection')
mongoose.connection.on('open', function() {
console.log('huzzah! connection open');
});
mongoose.connection.on('connecting', function() {
console.log('connecting');
});
mongoose.connection.on('connected', function() {
console.log('connected');
});
mongoose.connection.on('reconnected', function() {
console.log('reconnected');
});
mongoose.connection.on('disconnecting', function() {
console.log('disconnecting');
});
mongoose.connection.on('disconnected', function() {
console.log('disconnected');
});
mongoose.connection.on('error', function(error) {
console.log('error', error);
});
mongoose.connection.on('close', function() {
console.log('connection closed');
});
它是如何工作的
一般来说,连接到 MongoDB 并不复杂。它需要一个特定于 MongoDB 的统一资源标识符(uniform resource identifier,URI)方案,该方案将指向一个(或多个)可以托管您的 MongoDB 数据的服务器。在 Mongoose 中,使用了相同的 URI 模式,如清单 10-9 所示,增加了几个选项,如清单 10-8 所示。
清单 10-9 。MongoDB 连接字符串
mongodb://[username:password@]host[:port][[,host2[:port2]...[,hostN[:portN][/database][?options]
对于猫鼬,你用mongoose.connect(<uri>, <options>)的方法。您在 Mongoose 中设置的选项可以像表 10-4 中列出的任何选项一样进行设置。
表 10-4 。猫鼬连接选项
| [计]选项 | 描述 |
|---|---|
| 。作家(author 的简写) | 身份验证机制选项,包括要使用的机制的来源和类型。 |
| 。 | 传递给连接。数据库实例(例如,{native_parser: true}将使用本机二进制 JSON [BSON]解析)。 |
| .莽哥 | 布尔值,表示为您的. mongos 使用高可用性选项。如果连接到多个 Mongoose 实例,则应设置为 true。 |
| 。及格 | 与用户名关联的密码。 |
| 。replset | 这是要使用的副本集的名称,假设您要连接的 Mongoose 实例是副本集的成员。 |
| 。计算机 Web 服务器 | 传递给连接服务器实例(例如,{poolSize: 1}个池)。 |
| 。用户 | 用于身份验证的用户名。 |
connection 对象继承了 Node.js EventEmitter ,因此您可以从您的解决方案中看到,您可以使用 Mongoose 订阅许多事件。这些事件在表 10-5 中进行了概述和描述。
表 10-5 。猫鼬连接事件
| 事件 | 描述 |
|---|---|
| '关闭' | 在所有连接上执行 disconnected 和“onClose”后发出。 |
| '已连接' | 成功连接到数据库后发出。 |
| '正在连接' | 对连接执行 connection.open 或 connection.openSet 时发出。 |
| '断开连接' | 断开连接后发出。 |
| '断开连接' | 执行 connection.close()事件时发出。 |
| '错误' | 当错误发生时(即当 Mongo 实例被删除时)发出。 |
| '完整设置' | 当所有 Node 都连接时,在副本集中发出。 |
| '打开' | 一旦打开到 MongoDB 实例的连接就发出。 |
| '重新连接' | 在后续连接后发出。 |
这是使用 Mongoose 连接到 MongoDB 的基本场景和设置。在下一节中,您将研究如何使用 Mongoose 智能地建模数据存储并在 MongoDB 中检索它。
10-5.猫鼬的建模数据
问题
您希望在 Node.js 应用中使用 Mongoose 对 MongoDB 数据建模。
解决办法
用 Mongoose 建模数据时,需要利用mongoose.model()方法 。您不一定需要mongoose.Schema方法,但是对于在清单 10-10 中创建的模型,它被用来构建模型的模式。
清单 10-10 。用猫鼬创建模型
/**
* Modeling data with Mongoose
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
mongoose.connect('mongodb://localhost/test');
var productModel = new Schema({
productId: ObjectId,
name: String,
description: String,
price: Number
});
var Product = mongoose.model('Product', productModel);
var sandal = new Product({name: 'sandal', description: 'something to wear', price: 12});
sandal.save(function(err) {
if (err) console.log(err);
console.log('sandal created');
});
Product.find({name: 'sandal'}).exec(function(err, product) {
if (err) console.log(err);
console.log(product);
});
它是如何工作的
对于在 Node.js 应用中使用 MongoDB 的人来说,mongose 成为高优先级的原因之一是因为 mongose 自然地对数据建模。这意味着您只需要为您的数据生成一个模式模型,然后就可以使用该模型从 MongoDB 获取、更新和删除数据。
在您的解决方案中,首先导入mongoose.Schema对象。模式是通过传递一个 JavaScript 对象来创建的,该对象包含您希望建模的实际模式,以及一个可选的第二个参数,该参数包含您的模型的选项。该模式不仅允许您在模型中创建字段的名称,还为您提供了为模式中的值命名特定类型的机会。模式实例化中实现的类型显示为{ <fieldname> : <DataType> }。可用类型如下:
- 排列
- 布尔缓冲区
- 日期
- 混合的
- 数字
- ObjectId(对象 Id)
字符串您为您的模式创建的选项是在表 10-6 中显示的任何选项。
表 10-6 。猫鼬的选择。计划
| [计]选项 | 描述 |
|---|---|
| 自动索引 | 决定 MongoDB 是否自动生成索引的布尔值。默认值:真 |
| buffer 命令 | 一个布尔值,它决定当连接丢失时命令是否被缓冲,直到重新连接发生。默认值:真 |
| 脱帽致意 | 将 MongoDB 设置为有上限——这意味着集合的大小是固定的。默认值:False |
| 募捐 | 设置集合名称的字符串。 |
| 编号 | 返回文档的 _id 字段,或对象的十六进制字符串。如果设置为 false,这将是未定义的。默认值:真 |
| _id | 告知 MongoDB 是否会在创建模型对象时创建 _id 字段。默认值:真 |
| 阅读 | 在架构上设置 query.read 选项。这个字符串决定了您的应用是从复制集中的主要、辅助还是最近的 Mongo 读取。选项:'主要' '主要优先' '次要' '次要优先' '最近' |
| 安全的 | 布尔值,它设置是否将错误传递给回调。默认值:真 |
| 分布式 | 设置以哪个分片集合为目标。 |
| 严格的 | 确保传递给构造函数的非模型值不被保存的布尔值。默认值:真 |
| 托杰森 | 将模型转换为 JavaScript 对象表示法(JSON)。 |
| 图征物件 | 将模型转换为普通的 JavaScript 对象。 |
| 版本密钥 | 创建模型时设置模式的版本。默认值:__v: 0 |
当您创建您的“产品”模式时,您创建了一个包含一个productId的简单对象,它导入产品的ObjectId或hexString。您的模型还将为您希望存储和检索的产品创建一个字符串形式的名称、一个字符串形式的描述和一个数字形式的价格。
从 schema 对象中,您现在实际上创建了一个 Mongoose 模型,方法是使用mongoose.model()方法并传递您为模型选择的名称和 schema 模型本身。现在,您可以使用这个新产品模型来创建产品。您可以通过在 MongoDB 服务器上传递您希望在文档中建模的对象来做到这一点。在此解决方案中,您将创建一个凉鞋对象。然后通过使用接受回调的sandal.save()方法来保存它。
您还可以从您的模型中查找和删除数据。在这个解决方案中,您使用Product.find({ name: 'sandal' }),查询您的模型,它将搜索所有名为“sandal”的产品,并在 exec()回调中返回这些产品。从回调中,您可以访问名为“sandal”的所有产品的数组。如果您希望删除全部或部分结果,您可以遍历这些结果并逐个删除它们,如清单 10-11 所示。
清单 10-11 。使用猫鼬删除记录
Product.find({name: 'sandal'}).exec(function(err, products) {
if (err) console.log(err);
console.log(products);
for (var i = 0; i < products.length; i++) {
if (i >= 3) {
products[i].remove(function() {
console.log('removing');
});
}
}
});
您已经看到了如何在 Node.js 应用中使用 Mongoose 连接和实现一个模式。Mongoose 对象模型允许将您的模型干净地实现到 MongoDB 文档数据库。
10-6.连接到 CouchDB
问题
您希望在 Node.js 应用中利用 CouchDB 。
解决办法
CouchDB 是一个数据库,它利用 JSON 文档、HTTP 的应用编程接口(API)和 JavaScript 的 MapReduce。正因为如此,成为了很多 Node.js 开发者的天然契合点。有几个模块可用于使用 CouchDB 构建 Node.js 应用。在这个解决方案中,您将利用 Nano,这是一个支持 CouchDB 的轻量级模块。可以使用$ npm install nano 进行安装。
在清单 10-12 中,您将创建一个数据库并将一个文档插入到该数据库中。接下来,您将更新数据库中文档。然后,您将在清单 10-13 中检索文档并将其从数据库中删除。
清单 10-12 。用 Nano 在 CouchDB 中创建数据库和文档
/**
* CouchDB
*/
var nano = require('nano')('http://localhost:5984');
nano.db.create('products', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
var products = nano.db.use('products', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
products.insert({ name: 'sandals', description: 'for your feet', price: 12.00}, 'sandals', function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
products.get('sandals', {ref_info: true}, function(err, body, header) {
if (err) console.log(err);
console.log(body, header);
});
// Updating in couchDB with Nano.
products.get('sandals', function(err, body, header) {
if (!err) {
products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev }, 'sandals', function(err, body, header) {
if (!err) {
console.log(body, header);
}
});
}
});
清单 10-13 。从 CouchDB 数据库中删除文档
var nano = require('nano')('http://localhost:5984');
var products = nano.db.use('products');
// deleting in couchDB with Nano.
products.get('sandals', function(err, body, header) {
if (!err) {
products.destroy( 'sandals', body._rev, function(err, body, header) {
if (!err) {
console.log(body, header);
}
nano.db.destroy('products');
});
}
});
您还可以通过一个单一的nano.request()接口使用 Nano 创建对 CouchDB 的请求,如清单 10-14 所示。
清单 10-14 。使用 nano.request()
nano.request({
db: 'products',
doc: 'sandals',
method: 'get'
}, function(err, body, header) {
if (err) console.log('request::err', err);
console.log('request::body', body);
});
它是如何工作的
使用 CouchDB 是许多 Node.js 应用的天然选择。Nano 模块被设计为“Node.js 的极简 CouchDB 驱动程序”,不仅是 Nano 极简,而且它还支持使用管道,您可以从 CouchDB 直接访问错误。
在清单 10-12 中,您首先需要 Nano 模块并连接到您的服务器。连接到服务器就像指向包含正在运行的 CouchDB 实例的主机和端口一样简单。接下来,在 CouchDB 服务器上创建一个数据库,它将保存您希望创建的所有产品。当您使用 Nano 调用任何方法时,您可以添加一个回调来接收错误、主体和头参数。当你第一次用 Nano 创建数据库时,你会看到主体和请求响应 JSON,看起来像清单 10-15 。
清单 10-15 。创建“产品后回调
Body: { ok: true }
Header: { location: 'http://localhost:5984/products',
date: 'Sun, 28 Jul 2013 14:34:01 GMT',
'content-type': 'application/json',
'cache-control': 'must-revalidate',
'status-code': 201,
uri: 'http://localhost:5984/products' }
一旦创建了数据库,就可以创建第一个产品文档。通过将包含产品信息的 JavaScript 对象传递给products.insert() 方法,可以创建“sandals”文档。第二个参数是您希望与该文档相关联的名称。正文和标题的响应会让你知道插入是正确的,如清单 10-16 中的所示。
清单 10-16 。插入产品
Body: { ok: true,
id: 'sandals',
rev: '1-e62b89a561374872bab560cef58d1d61' }
Header: { location: 'http://localhost:5984/products/sandals',
etag: '"1-e62b89a561374872bab560cef58d1d61"',
date: 'Sun, 28 Jul 2013 14:34:01 GMT',
'content-type': 'application/json',
'cache-control': 'must-revalidate',
'status-code': 201,
uri: 'http://localhost:5984/products/sandals' }
如果您想用 Nano 更新 CouchDB 中的一个文档,您需要获得您希望更新的特定文档的修订标识符,然后再次调用nano.insert()函数,将修订标识符传递给您希望更新的特定文档。在您的解决方案中,您通过使用nano.get()方法,然后使用来自回调的 body._rev修订标识符来更新文档(参见清单 10-17 )。
清单 10-17 。更新现有文档
products.get('sandals', function(err, body, header) {
if (!err) {
products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev
}, 'sandals', function(err, body, header) {
if (!err) {
console.log(body, header);
}
});
}
});
创建、插入和更新文档后,您可能希望能够不时地删除项目。为此,您还需要一个对您计划删除的文档修订的引用。这意味着您可以首先用nano.get()获取文档,并使用来自回调的body._rev标识符传递给nano.destroy()方法。这将从数据库中删除该文档。但是,如果您想删除您的数据库,您可以通过调用nano.db.destroy(<DBNAME>);来销毁整个数据库。
Nano 框架的关键在于所有这些函数实际上都是你在清单 10-14 中看到的nano.request()方法的包装器。在清单 10-14 中,您的请求是以 HTTP GET 的形式向“产品”数据库中的“凉鞋”文档发出的。这个和nano.get()一样。销毁操作的等效操作是使用 HTTP 动词 DELETE,因此在您的nano.db.destroy('products')的实例中,您实际上是在编写nano.request({db: 'products', method: 'DELETE'}, callback);.
10 比 7。使用 Redis
问题
您希望在 Node.js 应用中利用 Redis 键值存储。
解决办法
Redis 是一个非常强大和流行的键值数据存储。因为它太受欢迎了,所以 Node.js 有很多实现可供选择。一些被用来连接到 Express web 服务器框架,而其他的只是指定的。Redis 网站上推荐的一个实现是 node_redis,位于https://github.com/mranney/node_redis。要安装 node_redis,可以按如下方式利用 NPM:$ npm install redis。
对于熟悉 redis 的人来说,使用 redis_node 很简单,因为 API 是相同的。您的所有 get、set、hget 和 hgetall 命令都可以直接从 Redis 本身执行。清单 10-18 中显示了一个获取和设置值和哈希值的简单示例。
清单 10-18 。用 node_redis 获取和设置字符串和散列键值对
/**
* Redis
*/
var redis = require("redis"),
client = redis.createClient();
client.on("error", function (err) {
console.log("Error " + err);
});
client.set("key", "value", redis.print);
client.hset("hash key", "hashtest 1", "some value", redis.print);
client.hset(["hash key", "hashtest 2", "some other value"], redis.print);
client.hkeys("hash key", function (err, replies) {
console.log(replies.length + " replies:");
replies.forEach(function (reply, i) {
console.log(" " + i + ": " + reply);
});
client.quit();
});
client.hgetall('hash key', function(err, replies) {
replies.forEach(function(reply) {
console.log(reply);
});
});
client.get("key", function(err, reply) {
if (err) console.log(err);
console.log(reply);
});
其他时候,您可能希望实现一个松散耦合的发布和订阅范例,而不是仅仅为会话级的键值存储存储散列。对于许多 Node.js 应用的开发人员来说,这可能是一种非常熟悉的方法,他们已经熟悉了事件驱动的开发,但是希望利用 Redis 来实现这些目的。清单 10-19 中显示了一个使用发布和订阅的例子。
清单 10-19 。发布和订阅示例
/**
* Pub/Sub
*/
var redis = require("redis"),
subscriber = redis.createClient(),
publisher = redis.createClient();
subscriber.on("subscribe", function (topic, count) {
publisher.publish("event topic", "your event has occured");
});
subscriber.on("message", function (topic, message) {
console.log("message recieved:: " + topic + ": " + message);
subscriber.end();
publisher.end();
});
subscriber.subscribe("event topic");
它是如何工作的
一旦你通过$ npm install redis安装了 node_redis,你就可以访问 Node.js 中 redis 的完整实现。正如你在清单 10-18 中看到的,你可以很容易地利用redis.createClient()创建一个新的客户端。createClient()方法将创建一个到 Redis 实例的端口和主机的连接,默认为http://127.0.0.1:6379,然后将实例化一个 RedisClient 对象,如清单 10-20 所示。
清单 10-20 。Node_redis 创建客户端
exports.createClient = function (port_arg, host_arg, options) {
var port = port_arg || default_port,
host = host_arg || default_host,
redis_client, net_client;
net_client = net.createConnection(port, host);
redis_client = new RedisClient(net_client, options);
redis_client.port = port;
redis_client.host = host;
return redis_client;
};
RedisClient 继承了 Node.js EventEmitter,会发出几个事件,如 表 10-7 所示。
表 10-7 。再贴现事件
| 事件 | 描述 |
|---|---|
| '连接' | 此事件将与“就绪”同时发出,除非客户端选项“no_ready_check”设置为 true,在这种情况下,只有在建立连接后才会发出此事件。然后你就可以自由地向 Redis 发送命令了。 |
| '排水' | 当到 Redis 服务器的传输控制协议(TCP)连接已经缓冲但再次可写时,RedisClient 将发出“drain”。 |
| '结束' | 一旦到 Redis 服务器的客户端连接关闭,就会发出此事件。 |
| '错误' | 当 Redis 服务器出现异常时,RedisClient 将发出“error”。 |
| “闲置” | 一旦没有等待响应的未完成消息,RedisClient 将发出“idle”。 |
| 准备好了吗 | 一旦建立了到 Redis 服务器和的连接,客户端将发出“就绪”事件,服务器报告它已准备好接收命令。如果您在“就绪”事件之前发送命令,它们将在该事件发出之前排队并执行。 |
在您的解决方案中,然后设置一个字符串值和一个哈希值。使用client.set和client.get设置和检索字符串值。为了处理散列,您还使用了client.hset、client.hkeys和client.hgetall。这些方法直接等同于直接输入命令(见清单 10-21 )。
清单 10-21 。雷迪斯集、get、hset、hkeys 和 hgetall
> set key value
OK
> get key
"value"
> hset 'hash key' 'hashtest 1' 'blah'
(integer) 0
> hset 'hash key' 'hashtest 2' 'cheese'
(integer) 0
> hkeys 'hash key'
1) "hashtest 1"
2) "hashtest 2"
> hgetall 'hash key'
1) "hashtest 1"
2) "blah"
3) "hashtest 2"
4) "cheese"
然后,您创建了一个发布和订阅解决方案。这可以与 Node.js 的事件模型一起使用,以便在应用的隔离部分之间创建一个松散耦合的集成。首先,您创建了两个名为 publisher 和 subscriber 的RedisClients。首先,在您希望收听的主题上调用subscriber.subscribe(),然后一旦订户的‘subscribe’事件被发出,就使用publisher.publish(<event name>)实际发出该事件。然后,您可以将订阅者绑定到消息事件,并在该事件发布后执行各种操作。
现在,您已经利用 Redis 存储了键-值对,以及带有 node_redis 的数据存储中的散列键。您还使用 Redis 执行了发布和订阅方法来支持这些消息。
10-8.连接到卡珊德拉
问题
您正在利用 Cassandra 来记录来自您的应用的事件,并且您希望用 Node.js 来实现这种日志记录。
解决办法
在不同的编程语言中,Cassandra 有许多不同的驱动程序。对于 Node.js,与$ npm install helenus一起安装的包“helenus”处于最前沿,因为它提供了对 thrift 协议和 Cassandra 查询语言(CQL) 的绑定。
在清单 10-22 中,您将创建一个日志机制来记录 Node.js 服务器上发生的事件。
清单 10-22 。使用 helenus 为 Cassandra 创建一个日志应用
var helenus = require('helenus'),
pool = new helenus.ConnectionPool({
hosts : ['127.0.0.1:9160'],
keyspace : 'my_ks',
user : 'username',
password : 'pass',
timeout : 3000//,
//cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
});
var logger = module.exports = {
/**
* Logs data to the Cassandra cluster
*
* @param status the status event that you want to log
* @param message the detailed message of the event
* @param stack the stack trace of the event
* @param callback optional callback
*/
log: function(status, message, stack, callback) {
pool.connect(function(err, keyspace){
console.log('connected');
keyspace.get('logger', function(err, cf) {
var dt = Date.parse(new Date());
//Create a column
var column = {};
column['time'] = dt;
column['status'] = status;
column['message'] = message;
column['stack'] = stack;
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());
cf.insert(timeUUID, column, function(err) {
if (err) {
console.log('error', err);
}
Console.log('insert complete');
if (callback) {
callback();
} else {
return;
}
});
});
});
}
};
它是如何工作的
helenus 模块是连接到 Cassandra 数据库的健壮解决方案。在您的解决方案中,导入 helenus 模块后,您连接到 Cassandra。这是通过向helenus.connectionPool()传递一个简单的对象来实现的。创建这个连接池的对象包含几个选项,如 表 10-8 所示。
表 10-8 。连接池选项
| [计]选项 | 描述 |
|---|---|
| 。cqlVersion | 命名您希望使用的 CQL 版本。 |
| 。主机 | 提供一个值数组,这些值是群集中所有 Cassandra 实例的 IP 地址和端口。 |
| 。keyspace(键空间) | 列出您希望最初连接到的 Cassandra 集群上的密钥空间。 |
| 。密码 | 提供您希望用来连接到 Node 的密码。 |
| 。超时 | 超时时间,以毫秒为单位。 |
| 。用户 | 给出连接到 Node 的用户名。 |
一旦建立了连接,您就可以调用pool.connect()。一旦连接发生,回调将提供对您在连接池中配置的默认键空间的引用。然而,有另一种方法可以通过使用pool.use('keyspacename', function(err, keyspace) {});方法连接到一个键空间。
现在,您可以访问 Cassandra 集群上的密钥空间。要访问 logger 列族,您可以调用keyspace.get('logger'...),它将获取列族并返回一个引用,这样您就可以直接对列族进行操作。
现在您已经获得了对希望写入数据的列族的访问权,您可以创建想要插入的列了。在这个解决方案中,假设您的 logger 列族有一个 TimeUUID 类型的行键,为每个条目创建一个惟一的时间戳。 Helenus 允许您轻松使用这种类型的键,因为 TimeUUID 是一种内置类型。您可以访问这个类型,并通过在对象上使用fromTimestamp方法创建一个新的 TimeUUID,如清单 10-23 中的所示。您还将看到,如果需要,helenus 提供了一种生成 UUID 类型的方法。
清单 10-23 。创建新的 TimeUUID
> helenus.TimeUUID.fromTimestamp(new Date());
c19515c0-f7c4-11e2-9257-fd79518d2700
> new helenus.UUID();
7b451d58-548f-4602-a26e-2ecc78bae57c
除了 logger 列族中的行键之外,您只需传递希望记录的事件的时间戳、状态、消息和堆栈跟踪。这些都成为您命名为“列”的对象的一部分现在已经有了行键和列值,可以通过调用列族上的cf.insert方法将它们插入到 Cassandra 中。
这个解决方案利用 JavaScript 和对象生成一个类似模型的实现,该实现被转换成 Cassandra Thrift 协议,以便插入数据。Helenus 允许通过使用 CQL 语言插入数据的其他方法。与清单 10-22 中的类似的实现,但是使用了 CQL ,如清单 10-24 中的所示。检索列族的步骤被省略了,因为 CQL 直接在键空间上操作。
清单 10-24 。使用 CQL 将数据记录到卡珊德拉
var helenus = require('helenus'),
pool = new helenus.ConnectionPool({
hosts : ['127.0.0.1:9160'],
keyspace : 'my_ks',
user : 'username',
password : 'pass',
timeout : 3000//,
//cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
});
var logger = module.exports = {
/**
* Logs data to the Cassandra cluster
*
* @param status the status event that you want to log
* @param message the detailed message of the event
* @param stack the stack trace of the event
* @param callback optional callback
*/
log: function(status, message, stack, callback) {
pool.connect(function(err, keyspace){
keyspace.get('logger', function(err, cf) {
var dt = Date.parse(new Date());
//Create a column
var column = {};
column['time'] = dt;
column['status'] = status;
column['message'] = message;
column['stack'] = stack;
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());
var cqlInsert = 'INSERT INTO logger (log_time, time, status, message,stack)' +
'VALUES ( %s, %s, %s, %s, %s )';
var cqlParams = [ timeUUID, column.time, column.status, column.message, column.stack ];
pool.cql(cqlInsert, cqlParams, function(err, results) {
if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
});
});
});
}
};
var queueObj = {};
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date()) + '';
var cqlInsert = 'INSERT INTO hx_services_pha_card (card_id, card_definition_id, pig_query, display_template, tokens, trigger)' +
'VALUES ( %s, %s, %s, %s, %s, %s )';
var cqlParams = [ timeUUID, queueObj.card_definition_id, queueObj.pig_query, queueObj.display_template, tokens.join(','), queueObj.trigger ];
pool.cql(cqlInsert, cqlParams, function(err, results) {
if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
});
10-9.对 Node.js 使用 Riak
问题
您希望能够在 Node.js 应用中利用高度可伸缩的分布式数据库 Riak。
解决办法
Riak 是为分布式系统的高可用性而设计的。它被设计为快速和可伸缩的,这使得它非常适合许多 Node.js 应用。在清单 10-25 中,您将再次创建一个数据存储,它将创建、更新和检索您的产品数据。
注意 Riak 目前在 Windows 机器上不支持。下面的实现应该可以在 Linux 或 OSX 上工作。
清单 10-25 。通过 Node.js 使用 Riak
/**
* RIAK
*/
var db = require('riak-js').getClient();
db.exists('products', 'huaraches', function(err, exists, meta) {
if (exists) {
db.remove('products', 'huaraches', function(err, value, meta) {
if (err) console.log(err);
console.log('removed huaraches');
});
}
});
db.save('products', 'flops', { name: 'flip flops', description: 'super for your feet', price: 12.50}, function(err) {
if (err) console.log(err);
console.log('flip flops created');
process.emit('prod');
});
db.save('products', 'flops', { name: 'flip flops', description: 'fun for your feet', price: 12.00}, function(err) {
if (err) console.log(err);
console.log('flip flops created');
process.emit('prod');
});
db.save('products', 'huaraches', {name: 'huaraches', description: 'more fun for your feet', price: 20.00}, function(err) {
if (err) console.log(err);
console.log('huaraches created');
process.emit('prod');
db.get('products', 'huaraches', function(err, value, meta) {
if (err) console.log(err);
console.log(value);
});
});
process.on('prod', function() {
db.getAll('products', function(err, value, meta) {
if (err) console.log(err);
console.log(value);
});
});
它是如何工作的
为了创建这个解决方案,您从使用$ npm install riak-js安装的 Node.js 模块 riak-js 开始。然后你通过使用getClient()方法连接到服务器。这种方法在不使用的情况下,会发现默认的客户端运行在本地机器上,但是也可以用 options 对象进行配置。
现在,您通过使用 db 对象连接到了 Riak 实例。首先,当你遇到db.exists(<bucket>, <key>, callback)时,你看到 API 是简洁的。如果这个键存在于 Riak Node 上的 bucket 中,这个回调将返回 true 值。如果存储桶键确实存在,您只需指向该存储桶键并使用db.remove()方法就可以删除该特定数据集。
接下来,使用db.save方法将一些数据保存到 Riak Node。该方法接受一个桶、一个键和一个您希望为桶键设置的值。这可以是一个 JavaScript 对象、一个数字或一个您希望为键值存储的字符串。与 riak-js 的所有请求一样,您也可以访问回调函数。回调有三个值:一个错误(如果发生的话)、一个作为 riak-js 方法的结果传递的值和一个元对象。
在您将 huaraches 保存到产品存储桶之后,您可以通过使用db.get()功能来检索这个密钥。同样,这个方法使用桶和键来确定您希望检索 Node 上的哪些数据。回调可以包含与数据相关联的值和元。使用 riak-js 还有一种方法可以访问数据。这用于检索给定存储桶的所有值。回调中的结果值将是一个与桶相关联的数据数组。
您已经使用 riak-js 与 Node.js 中的 riak 集群进行了交互。Riak 是一个强大的分布式数据库解决方案,除了这些简单的任务之外,还可以通过类似的 API 使用 map 和 reduce 函数执行更复杂的搜索。为此,您可以通过运行以下命令来搜索您的 Node 中的所有产品(参见清单 10-26 )。
清单 10-26 。使用 riak-js 减少地图
db.mapreduce
.add('products')
.map(function(v) {
return [Riak.mapValuesJson(v)[0]];
})
.run(function(err, value, meta) {
console.log(value);
});
十一、在 Node.js 中测试
测试让您安心地知道您编写的代码实际上正在执行它想要做的操作。Node.js 为编写某种形式的单元测试提供了本机实现,Node.js 社区已经创建了几个健壮的库来帮助您进行测试驱动的开发过程。
在本章中,您将研究如何为 Node.js 应用决定一个测试框架,并且您还将看到各种各样的技术和测试机制,您可以使用它们来创建一个完全经过测试的应用。一旦您构建了 Node.js 应用所需的测试,您将学习如何报告您已经创建的测试并将它们集成到您的工作流中。
当您创建 Node.js 测试实现时,有很多选项可供您使用。在本章中,您将有机会看到一些框架的小样本,包括以下内容:
- Node.js 断言
- Node 单元
- 摩卡
- 柴网
- 誓言. js
- 应该. js
本章中使用的那些在社区中更为突出,但无论如何这不是一个详尽的列表。本章旨在向您提供足够的信息,以便为 Node.js 应用创建一个完整的测试套件,但是我将尝试对您最终可能选择的框架保持不可知的态度。
11-1.选择测试驱动开发或行为驱动开发的框架
您希望在 Node.js 开发过程中实现测试。要做到这一点,您需要找到满足您测试需求的最佳测试解决方案。
当你选择一个测试框架时,你应该考虑的第一件事是你要做的测试的类型。有两个通用的类别来描述测试风格,它们可以被分成更小的类别。
首先,有经典的测试驱动开发(TDD) 。TDD 是有用的,因为你接受一个代码需求,为需求的那个方面写一个失败的测试,然后创建允许测试通过的代码。TDD 的演变是行为驱动开发(BDD)。 BDD 不仅采用可测试的代码功能单元,还采用可测试的业务用例,保护代码和最终用户的体验。
您的框架选择不仅取决于您喜欢的测试风格,还取决于您希望遇到的测试类型。您可能希望仅对应用中的关键值进行一些简单的断言测试。您可能希望提供一个代码覆盖率总结,让您知道可以在哪里重构并从应用中删除无关的代码。
在本章中,您将看到许多实现,每个实现都提供了 Node.js 中测试的独特方式。这不是一个详尽的列表,但旨在帮助您了解如何选择一个适合您的测试框架。
有些与异步代码无缝协作;其他人在大规模测试项目中表现更好。这些决定取决于您,即开发人员,来决定哪一个适合您的需要。应该选择这些框架中的任何一个还是都不选择,都不如将测试引入工作流并构建一个 Node.js 应用重要,因为您知道该应用已经过很好的测试。
11-2.使用 Node.js 断言模块创建测试
问题
您希望利用 Node.js 的本机 assert 模块来测试您的 Node.js 应用。
解决办法
在 Node.js 中创建基本测试有许多选项。许多测试的基础都源自 Node.js assert 模块。这个模块在内部用于 Node.js 测试,是 TDD 的一个很好的模型。
清单 11-1 展示了如何用 Node.js 断言模块实现一组基本的测试。
清单 11-1 。Node.js 断言基础
var assert = require('assert');
/*11-2*/
var three = 3;
assert.equal(three, '3', '3 equals "3"');
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
assert.notEqual(three, 'three', '3 not equals three');
// assert.ok(false, 'not truthy ');
assert.ok(true, 'truthy');
Node.js 断言模块也可以用来测试异步代码,如清单 11-2 所示。
清单 11-2 。测试异步平方一个数字的函数
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
var assert = require(‘assert’);
squareAsync(three, function(result) {
assert.equal(result, 9, '3 squared is nine');
});
squareAsync('three', function(result) {
assert.ok(isNaN(result), '"Three squared is NaN');
});
通常在 Node.js 中,您创建的代码是异步的,如上所示。然而,有时您可能有一些想要测试的同步性质的代码。你可以按照清单 11-3 中的例子用断言模块测试同步代码。
清单 11-3 。与 Node.js 断言同步
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var assert = require('assert');
assert.equal(square(three), 9, '3 squared equals 9');
assert.equal(square('three'), false, 'should fail because "three" is not a number');
它是如何工作的
使用 require('assert ')导入的 Node.js 断言测试模块用于内部 Node.js 测试,并已成为许多第三方测试实现的基础。assert 模块是以这样一种方式创建的,如果不满足测试需要的特定条件,它将抛出一个断言错误。如果全部通过,Node.js 进程将按预期退出。
在此解决方案中,您看到了可以通过使用 assert 模块的应用编程接口(API) 的一小部分来验证真值或假值、等式和非等式。API 能够断言的完整列表如表 11-1 所示。
表 11-1 。断言模块的 API 方法
| 方法 | 描述 |
|---|---|
| assert.deepEqual(实际、预期、消息) | 深度相等的测试。这将测试' === '运算符以外的内容,并检查比较中的日期、正则表达式、对象和缓冲区。 |
| assert.doesNotThrow(块,[错误],[消息]) | 期望提供的代码块不会引发错误。 |
| assert.equal(实际、预期、消息) | 使用“==”运算符测试实际值是否等于预期值。 |
| assert.fail(实际、预期、消息、运算符) | 通过显示消息以及由运算符分隔的实际值和期望值来引发断言异常。 |
| assert.ifError(值) | 测试该值是否不为假,如果为假,则引发断言异常。这用于测试回调是否提供了错误参数。 |
| assert.notDeepEqual(实际,预期,消息) | 深度非质量测试。这超出了测试的范围!== '运算符,并在比较中检查日期、正则表达式、对象和缓冲区。 |
| assert.notEqual(实际,预期,消息) | 使用“!= '运算符。 |
| assert.notStrictEqual(实际,预期,消息) | 与相同。notEqual 的不同之处在于,它将这些值与“!== '运算符。 |
| assert.ok(值,消息) | 测试传递的值是否为真值。否则,消息将被记录为断言异常。 |
| assert.strictEqual(实际、预期、消息) | 与相同。等于,不同之处在于它使用“===”运算符来比较这些值。 |
| assert.throws(块,[错误],[消息]) | 期望提供的块引发错误。 |
11-3.使用 Node 单元创建测试
问题
您希望利用 nodeunit 单元测试模块来测试 Node.js 应用。
解决办法
nodeunit 测试模块建立在 assert 模块的 API 之上,如清单 11-4 所示。
清单 11-4 。使用 Node 单元进行测试
var test = require('nodeunit');
module.exports = {
'11-2': {
'equal': function(test) {
test.equal(3, '3', '3 equals "3"');
test.done();
},
'strictEqual': function(test) {
test.strictEqual('3', 3, '3 and "3" are not strictly equal');
test.done();
},
'notEqual' : function(test) {
test.notEqual(3, 'three', '3 not equals three');
test.done();
},
'ok' : function(test) {
test.ok(false, 'not truthy ');
test.done();
}
}
};
清单 11-5 展示了利用 nodeunit 测试框架来创建异步方法测试的解决方案。
清单 11-5 。Node 单元异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
module.exports = {
'11-4': {
'squareAsync': function(test) {
test.expect(2);
squareAsync(three, function(result) {
test.equal(result, 9, 'three squared is nine');
});
squareAsync('three', function(result) {
test.ok(isNaN(result), 'squaring a string returns NaN');
});
test.done();
}
}
};
Nodeunit 还允许同步测试(如清单 11-6 所示)。
清单 11-6 。Node 单元同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var test = require('nodeunit');
module.exports = {
'11-3': {
'squareSync': function(test) {
test.equal(square(three), 9, 'three squared is nine');
test.equal(square('three'), false, 'cannot square a non number');
test.done();
}
}
};
它是如何工作的
Nodeunit 提供了与 assert 模块相同的 API,并增加了两个方法。首先,添加。done()方法,它告诉 nodeunit 您已经完成了那个测试,是时候进行下一个测试了。nodeunit 带来的第二个附加功能是。期望(金额)法。该方法允许您指示 nodeunit 您计划实现多少测试,并确保所有测试都已执行。如果您已经指示 nodeunit 使用 expect(2)运行两个测试,并且只运行了一个,那么测试将失败,并显示消息:Error: Expected 2 assertions, 1 ran.您将在下一节中看到,能够使用 expect()和 done()方法提供了一种测试异步代码的好方法。
要用 nodeunit 实现测试,您需要从一个模块中导出测试。正如您之前看到的,您首先需要 nodeunit 模块,然后导出您的测试。这种导出的格式可以帮助您对测试进行分组。您可以在解决方案中看到,您将本部分的所有测试分组到了 11-2 标题下。然后您分别创建每个测试,调用每个测试的断言,然后调用test.done()。
要调用 nodeunit 测试,必须先用npm install –g nodeunit安装 nodeunit。然后瞄准你想测试的模块。如果您使用一个名为 nodeunit.js 的文件,您将调用$ nodeunit nodeunit.js,测试将执行并产生一个类似于您在清单 11-7 中看到的结果,带有复选标记来表示通过测试,带有“X”表示失败。
清单 11-7 。Node 单元结果
nodeunit.js
✔ 11-2 - equal
✔ 11-2 - strictEqual
✔ 11-2 - notEqual
✖ 11-2 – ok
11-4.用 Mocha 创建测试
问题
您需要利用 Mocha 为 Node.js 应用创建测试。
解决办法
用于编写 Node.js 测试的一个著名框架是 Mocha 。如何使用它的一个例子显示在清单 11-8 中。
清单 11-8 。使用 Mocha 框架
var assert = require('assert');
var three = 3;
describe('11-2', function() {
describe('#equal', function() {
it('should return true that 3 equals "3"', function() {
assert.equal(three, '3', '3 equals "3"');
})
})
describe("#strictEqual", function() {
it('"3" only strictly equals 3.toString()', function() {
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
})
})
describe("#notEqual", function() {
it('should be that 3 is not equal to three', function() {
assert.notEqual(three, 'three', '3 not equals three');
})
})
describe("#ok", function() {
it('should return that false is not truthy', function() {
assert.ok(false, 'not truthy ');
})
})
describe("#ok", function() {
it('should be true that true is truthy', function() {
assert.ok(true, 'truthy');
})
})
});
Mocha 也可以测试异步代码,如清单 11-9 中的所示。
清单 11-9 。Mocha 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
result.should.not.equal(result);
});
有时,您会想要测试同步编写的代码。Mocha 可以像这样测试代码,如清单 11-10 所示。
清单 11-10 。摩卡同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
describe('11-3 sync', function() {
describe('square a number', function() {
it('should do this syncronously', function() {
square(three).should.equal(9);
});
it('should fail when the target is not a number', function() {
square('three').should.be.false;
});
});
});
它是如何工作的
Mocha 是一个测试框架,可以利用您选择的任何断言模块。在这个例子中,您利用 Node.js assert 模块来管理您的简单断言。然而,为了让这些断言工作,您必须将您的断言包装在特定于 Mocha 的描述中。这允许您命名您的测试套件和特定案例,以便从您的测试中看到想要的输出。
为测试创建描述的方法是将它们包装在函数“describe”中。这个函数将接受一个名称和一个回调函数。测试套件将单独的测试用例嵌套在其回调函数中。一般来说,这应该看起来像清单 11-11 中的例子。
清单 11-11 。用 Mocha 构建测试套件
describe(‘Test suite’, function() {
describe(‘test-case’, function() {
// tests go here
});
});
Mocha 中的测试可以包含对预期结果的描述,当您遇到断言错误时会用到它。语法读起来几乎像一个句子,正如你在清单 11-12 中看到的,在那里你描述了测试应该得到什么结果,并且测试本身包含在回调中。测试描述只有在测试失败时才会出现。
清单 11-12 。测试失败描述
it('should return true that 3 equals "3"', function() {
assert.equal(three, '3', '3 equals "3"');
});
一旦通过$ npm install –g mocha在全球范围内安装了 Mocha,有两种主要的方法可以让 Mocha 执行测试。可以直接在一个文件上调用 Mocha,通过调用$ mocha filename.js。如果您正在测试一个单独的测试文件,这是很有价值的,但是如果您想要测试多个文件,您可以简单地创建一个测试/目录,并将您的 Mocha 测试文件移动到该目录中。一旦你完成了这些,只需调用$ mocha,它就会找到那个目录并遍历它,执行遇到的测试。
11-5.用 Chai.js 创建测试
问题
您希望使用 Chai.js 测试框架创建测试。
解决办法
包含 Node.js assert 模块 API 的测试框架的另一个例子是 Chai.js 测试框架。清单 11-13 中的显示了相同的基本测试结构的一个例子。
清单 11-13 。使用 Chai.js 进行测试
/**
* chaijs
*/
var chai = require('chai');
/* 11-2 simple */
var assert = chai.assert;
var three = 3;
assert.equal(three, '3', '3 equals "3"');
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
assert.notEqual(three, 'three', '3 not equals three');
assert.ok(true, 'truthy');
//assert.isFalse(true, 'true is false');
var expect = chai.expect;
expect(three).to.be.a('number');
var should = chai.should();
three.should.be.a('number');
接下来,您可以使用 Chai.js 框架创建异步测试。这些显示在清单 11-14 中。
清单 11-14 。Chai.js 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
expect(result).to.not.eql(result);
});
清单 11-15 显示了如何使用 Chai.js 实现同步测试。
清单 11-15 。Chai.js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var chai = require('chai');
var should = chai.should();
square(three).should.equal(9);
square('three').should.be.false;
它是如何工作的
Chai.js 可以作为测试驱动,附带任意 Node.js 断言模块。通过安装 Chai.js 模块并将其导入本地,开始使用 Chai 进行测试。然后,您可以用 Chai.js 提供的几种风格中的任何一种来构建您的断言。
Chai.js 为 BDD 风格的断言提供了两种方法。首先是“期望”风格。expect 风格创建了一个类似句子的结构来描述你对代码的期望。您可以构建一个如下所示的语句:expect('cheese').to.be.a('string');。当您遇到像expect(3).to.be.a('string'), or expect(3).to.equal(6);这样的语句时,这种类型的测试会抛出断言错误。
BDD 风格的第二种类型是‘应该’。“Should”在许多方面类似于“expect”模块,尽管实现“should”模块需要您执行“should”函数chai.should(),而不是像在chai.expect中那样引用函数。这是因为当您调用‘expect’时,您调用的是您正在测试的对象的函数:expect(three)。在“应该”风格中,你从你正在测试的对象开始,然后描述应该发生的行为。
如表 11-2 所示,‘should’和‘expect’样式共享相同的 API。它们还提供了一组非测试性的方法,用于以可链接的方式构建类似句子的结构。这些方法是 to,be,been,is,that,and,have,with,at,of,some。
表 11-2 。“应该”和“期望”模块的 API 方法
| 方法 | 描述 |
|---|---|
| 。高于(值) | 断言目标大于值。 |
| 。争论 | 目标必须是 arguments 对象。 |
| 。a(类型) | 确定前面的对象应为“type”类型。 |
| 。低于(值) | 目标小于值。 |
| 。closeTo(预期,增量) | 目标值在给定的期望值加上或减去一个差值的范围内。 |
| 。包含(值) | 断言前面包含给定的值,即 expect(' cheese ')to . contain(' he ')。 |
| 。深的 | 这应该与下面的 equal 或 property 方法一起使用。将对断言执行深度相等检查。 |
| 。空的 | 断言目标的长度为 0,或者对象中没有可枚举的键。 |
| 。eql(值) | 目标必须深度相等。 |
| 。相等(值) | 目标必须严格相等。如果深度方法在它之前,则执行深度相等检查。 |
| 。存在 | 目标不能为空或未定义。 |
| 。错误的 | 目标必须是假的。 |
| 。包括(值) | 断言前面包含给定的值,即 expect([1,2,4]). include(2);。 |
| 。instanceof(构造函数) | 目标必须是构造函数的实例。 |
| 。它自己 | 设置供 respondTo 方法稍后使用的自身标志。 |
| 。按键(按键 1,[按键 2],…,[按键]) | 目标正好包含所提供的键。 |
| 。最少值 | 目标值必须大于或等于该值,..is.at.least(值)。 |
| 。长度(值) | 目标必须有长度值。 |
| 。匹配(正则表达式) | 目标匹配正则表达式。 |
| 。成员(集) | 目标具有集合的相同成员,或者是集合的超集。 |
| 。most(值) | 目标小于或等于该值。 |
| 。不 | 否定链中后面的任何断言。 |
| 。空 | 目标必须为空。 |
| 。好的 | 确保目标是真实的。 |
| .ownProperty(name) | 目标将有自己的属性名。 |
| 。属性(名称,[值]) | 目标将具有属性名,并且可选地,该值必须匹配。 |
| 。响应者(方法) | 目标将响应该方法。 |
| 。满足(方法) | 目标通过给定的真值测试。 |
| 。字符串(字符串) | 字符串目标必须包含字符串。 |
| 。throw(构造函数) | 函数目标应该抛出一个异常。 |
| 。真实的 | 目标必须是真的。 |
| 。不明确的 | 目标必须是未定义的。 |
| 。在(开始,完成)范围内 | 目标必须在开始和结束范围之间。 |
同样在这个例子中,Chai.js 断言模块中使用了经典的 TDD 风格。这个模块类似于 Node.js assert 模块,但是它将更多的方法合并到 assert API 中,如表 11-3 所示。
表 11-3 。Chai.js 断言
| 方法 | 描述 |
|---|---|
| 。closeTo(实际值,期望值,差值,[消息]) | 实际值将在预期值的差值范围内。 |
| 。deepEqual(实际,预期,[消息]) | 实际值必须与预期值相等。 |
| 。deepProperty(对象,属性,[消息]) | 对象必须包含属性,并且可以深度嵌套。 |
| 。deepPropertyNotVal(对象,属性,值,[消息]) | 对象必须包含深度属性,但值不是值。 |
| 。deepPropertyVal(对象,属性,值,[消息]) | 对象必须包含值为的深度属性。 |
| 。doesNotThrow(函数,[构造函数/正则表达式],[消息]) | 作为构造函数或匹配正则表达式的实例,函数不会出错。 |
| 。相等(实际,预期,[消息]) | 实际值必须不严格等于(==)预期值。 |
| 。失败(实际、预期、[消息]、[运算符]) | 抛出一个失败。 |
| 。包括(干草堆、针、[消息]) | 干草堆里有一根针。这可以用于字符串(包含)或数组。 |
| 。includeMembers(超集,子集,[消息]) | 子集必须包含在超集中。 |
| 。instanceOf(对象,构造函数,[消息]) | 对象必须是构造函数的实例。 |
| 。isArray(值,[消息]) | 该值必须是数组。 |
| 。isBoolean(值,[消息]) | 该值必须是布尔值。 |
| 。isDefined(值,[消息]) | 该值不得未定义。 |
| 。isFalse(值,[消息]) | 该值必须为假。 |
| 。isFunction(值,[消息]) | 该值必须是函数。 |
| 。isNotArray(值,[消息]) | 该值不能是数组。 |
| 。isNotBoolean(value,[message]) | 该值不能是布尔值。 |
| 。isNotFunction(值,[消息]) | 该值不能是函数。 |
| 。isNotNull(值,[消息]) | 该值不得为空。 |
| 。isNotNumber(值,[消息]) | 该值不能是数字。 |
| 。isnotobject(值,[消息]) | 该值不能是对象。 |
| 。isNotString(值,[消息]) | 该值不能是字符串。 |
| 。isNull(值,[消息]) | 该值必须为空。 |
| 。isNumber(值,[消息]) | 该值必须是一个数字。 |
| 。isObject(值,[消息]) | 该值必须是对象。 |
| 。isString(值,[消息]) | 该值必须是字符串。 |
| 。isTrue(值,[消息]) | 该值必须为真。 |
| 。I 未定义(值,[消息]) | 该值必须未定义。 |
| 。lengthOf(对象,长度,[消息]) | 对象必须具有给定的长度。 |
| 。匹配(值,正则表达式,[消息]) | 该值必须与正则表达式匹配。 |
| 。notDeepEqual(实际,预期,[消息]) | 实际值不能等于预期值。 |
| 。notDeepProperty(对象,属性,[消息]) | 对象不得包含深度属性。 |
| 。notEqual(实际,预期,[消息]) | 实际值必须是非严格的 in equal(!idspnonenote)值。=)到预期。 |
| 。未包括(干草堆、针、[消息]) | 干草堆里没有针。 |
| 。notInstanceOf(对象,构造函数,[消息]) | 对象不能是构造函数的实例。 |
| 。notMatch(value,regexp,[message]) | 该值不得与正则表达式匹配。 |
| 。notOk(对象,[消息]) | 对象必须不是真实的。 |
| 。notProperty(对象,属性,[消息]) | 对象不得包含属性。 |
| 。notStrictEqual(实际,预期,[消息]) | 实际值必须严格不相等(!idspnonenote)。==)到预期。 |
| 。notTypeOf(值,名称,[消息]) | 该值不是一种名称类型。 |
| 。确定(对象,[消息]) | 对象必须是真实的。 |
| 。操作员(val1,操作员,val2,[消息]) | val1 与 val2 的关系由运算符决定,即 operator(3,'>',0)。 |
| 。propertyNotVal(对象,属性,值,[消息]) | 对象必须包含属性,但值不是值。 |
| 。属性(对象,属性,[消息]) | 对象必须包含属性。 |
| 。propertyVal(对象,属性,值,[消息]) | 对象必须包含值为的属性。 |
| 。sameMembers(集合 1,集合 2,[消息]) | 集合 1 和集合 2 必须具有相同的成员。 |
| 。strictEqual(实际,预期,[消息]) | 实际值必须严格等于(===)预期值。 |
| 。throws(函数,[构造函数/字符串/正则表达式],[字符串/正则表达式],[消息]) | 该函数将作为构造函数的实例出错,或者它将抛出一个带有匹配 regexp/string 的错误。 |
| 。typeOf(值,名称,[消息]) | 该值必须是类型名称。 |
11-6.创建带有誓言的测试
问题
您需要使用誓言测试框架为 Node.js 应用创建测试。
解决办法
誓言是一个受 Node.js 开发人员欢迎的测试框架,因为它很好地配合了 Node.js 的异步特性。你可以在清单 11-16 中看到使用这个框架的基本解决方案。
清单 11-16 。使用 waves . js 进行测试
var vows = require('vows'),
assert = require('assert');
var three = 3;
vows.describe('vows testing').addBatch({
'11-2': {
'basic testing': {
topic: three,
' 3 equals "3"': function(topic) {
assert.equal(three, '3');
},
' 3 and "3" are not strictly equal': function(topic) {
assert.strictEqual('3', three.toString());
},
' 3 notEqual "three"' : function(topic) {
assert.notEqual(three, 'three');
},
' false is truthy? ' : function(topic) {
assert.ok(false);
},
' true is truthy? ' : function(topic) {
assert.ok(true);
}
}
}
}).export(module);
清单 11-17 。誓言. js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var vows = require('vows'),
assert = require('assert');
vows.describe('vows testing').addBatch({
'11-3': {
'sync': {
topic: function(a) { return 3 * 3; },
'squared': function(topic) {
assert.equal(topic, 9);
}
}
}
});
它是如何工作的
誓言可以说是与之前的测试框架风格最大的不同。誓言是为测试异步代码而构建的测试框架,因此它非常适合 Node.js。
在誓言中,以及在你使用誓言的解决方案中,你从描述一个测试套件开始。在我们的例子中,这是通过调用vows.describe(<suite name>)创建的“誓言测试”套件。然后,将一组测试分组到一个批处理中,并通过调用。addBatch()方法,它接受一个对象文字:测试批次。
然后将这些批次分组;在这种情况下,创建了 11-2 组,并用“基本测试”进一步细分。接下来,您将测试分成单独的主题。这里你处理变量“三”,它指向值 3。在主题中,您构建您的测试,首先描述您希望看到的结果,然后通过将主题传递给回调函数来执行测试。在这个回调中,您利用 Node.js assert 模块来验证您的测试。使用誓言的这个解决方案的测试输出可以通过两种方式生成,如清单 11-18 中的所示。
清单 11-18 。誓言输出选项
$ vows vows.js
···?·
11-2 basic testing
? false is truthy?
» expected expression to evaluate to true, but was false // vows.js:24
? Broken » 4 honored ∙ 1 broken
$ vows vows.js --spec
? vows testing
11-2 basic testing
√ 3 equals "3"
√ 3 and "3" are not strictly equal
√ 3 notEqual "three"
? false is truthy?
» expected expression to evaluate to true, but was false // vows.js:24
√ true is truthy?
? Broken » 4 honored ∙ 1 broken
简单地调用誓言方法将运行测试,但是通过在调用中添加- spec 参数,您能够获得更详细的结果输出。然后,它会打印出“兑现”誓言的数量:测试通过,或者失败,意味着测试失败。
在 Node.js 开发中,使用这些框架中的任何一个来设置测试都不应该是一个限制。这些框架应该成为您开发周期的一部分,并允许您构建一个更加成熟和可靠的 Node.js 应用。在接下来的部分中,您将看到如何测试同步方法的某些点,以及更常见的如何将这些测试框架融入到使用 Node.js 构建的异步应用中。
11-7.用 Should.js 创建测试
问题
您已经创建了一个 Node.js 应用,您需要使用 Should.js 框架为其创建测试。
解决办法
Should.js 是一个测试框架,它使用非常友好和可链接的 BDD 语法进行测试。使用 Should.js 的一个基本例子显示在清单 11-19 中。
清单 11-19 。使用 Should.js
var should = require('should');
var three = 3;
should.equal(three, '3', '3 equals "3"');
should.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
should.notEqual(three, 'three', '3 not equals three');
true.should.be.ok;
false.should.not.be.ok;
three.should.be.a('number');
Should.js 也可以用于异步和同步测试,如清单 11-20 和清单 11-21 所示。
清单 11-20 。Should.js 异步测试
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
squareAsync(three, function(result) {
result.should.equal(9, 'three squared is nine');
});
squareAsync('three', function(result) {
// NaN !== NaN
result.should.not.equal(result);
});
清单 11-21 。Should.js 同步测试
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
var should = require('should');
square(three).should.equal(9);
square('three').should.be.false;
它是如何工作的
当您开始使用 Should.js 进行测试时,您会注意到它是作为 Node.js assert 模块的扩展开始的。这意味着您可以像平常一样实现 assert API 的所有项目。Should.js 还扩展了这个模块,以包含一个名为。exists()与.ok()相同。
Should.js 和 Chai.js 的“Should”风格一样,允许测试链接。这不仅有助于更好地描述行为,还允许测试框架成为一段自文档化的代码。可链接语法允许链接方法,如 an、and、be、have 和 with。这些方法实际上并不操作测试,但是便于链接和可读性。Should.js 的 API 包含表 11-4 中所示的方法。
表 11-4 。Should.js API
| 方法 | 描述 |
|---|---|
| 。a | 目标应该是声明的类型。 |
| 。超过 | 目标必须有一个高于期望值的数值。 |
| 。争论 | 目标是一个类似于 arguments 数组的对象。 |
| 。在下面 | 目标值必须低于预期值。 |
| 。空的 | 目标的长度必须为 0。 |
| 。网站 | 检查是否相等。 |
| 。平等的 | 具有严格的平等性。 |
| 。错误的 | 目标必须=== false。 |
| 。标题(字段,[值]) | 目标必须是带有可选值相等检查的指定字段。 |
| 。超文本标记语言 | 目标必须是“text/html,charset=utf-8”。 |
| 。include SQL(obj) | 对象“obj”必须存在并且相等。 |
| 。包括(对象) | “obj”对象必须存在于的索引中。适用于字符串、数组和对象。 |
| .instanceOf | 目标应该是声明的的实例。 |
| 。数据 | 目标必须是“application/json,charset=utf-8”。 |
| 。键 | 目标必须具有指定的键。 |
| 。长度 | 目标必须是指定的长度。 |
| 。比赛 | 目标必须与正则表达式匹配。 |
| 。好的 | 目标必须是真实的。 |
| 。自有财产 | 目标必须指定自己的属性。 |
| 。财产 | 目标必须包含指定的属性。 |
| 。状态(代码) | 目标状态代码必须是指定的。 |
| 。扔过来。投掷误差 | 确保引发异常。如果您的 JavaScript Linter 不能友好地解析。扔命名法。 |
| 。真实的 | 目标必须=== true。 |
| 。在…之内 | 目标应该在一个数字范围内。 |
可以看到这个 API 类似于 Chai.js 框架使用的‘should’和‘expect’API。这是因为这种类型的测试的链接语法有助于 API 中的许多特性。
在清单 11-6 中,您使用 assert 模块语法构建了一些简单的测试。然后通过使用可链接的 Should.js 语法进入 BDD 模型:true.should.be.ok;,就是一个例子。
11-8.使用 Node 覆盖报告代码覆盖
问题
您已经为 Node.js 应用构建了一些测试。现在,您想分析您编写的代码是否会被 node-cover 完全覆盖。
解决办法
有几个库可用于报告 Node.js 应用中的代码覆盖率。对于这个解决方案,您将检查其中之一:Node 覆盖。这个库可以使用 npm: $ npm install cover –g进行全局安装。一旦安装了它,您就可以在 Node.js 应用上运行覆盖率测试和报告。
在本节中,您将创建一个简单的 Node.js 应用,它将有目的地测试您的代码覆盖率,并有助于突出 Node.js 覆盖率库的优势。这个示例代码如清单 11-22 所示。
清单 11-22 。代码覆盖率示例应用
/**
* coverage tests
*/
function add(a, b, callback) {
callback(a + b);
}
function subtract(a, b, callback) {
callback(a - b);
}
add(2, 3, function(result) {
console.log(result);
});
//subtract(3, 2, function(result) {
// console.log(result);
// });
您现在可以开始对这个代码块进行代码覆盖测试了。首先,安装 node-cover,然后运行覆盖率测试,如清单 11-23 所示。
清单 11-23 。安装 Node 覆盖并运行覆盖测试
$ npm install -g cover
$ cover run 11-5-1.js
现在,您已经成功地为您的代码执行了覆盖测试。使用 node-cover,您可以通过两种方式查看该覆盖率的报告。首先,清单 11-24 展示了如何在命令提示符/终端中直接查看覆盖率。
清单 11-24 。终端中的 Node 覆盖报告
> cover run 11-5-1.js
path.existsSync is now called `fs.existsSync`.
5
在终端中查看报告后,您可能会决定,为了更好地查看结果,或者将结果发布给对您的报道感兴趣的其他方,您应该以 HTML 模式查看报告。Node 覆盖使 HTML 中的报告变得简单,如清单 11-25 所示。在这里,该模块将生成一个 cover-html 目录,并允许您在浏览器中导航到/cover_html/index.html,以便查看如图图 11-1 和图 11-2 所示的报告。
清单 11-25 。报告 HTML 中的 Node 覆盖范围。
$ cover report html
图 11-1 。Node _ 覆盖 index.html
图 11-2 。Node _ 封面细节
注意由于字体渲染的问题,在 Windows 上的命令提示符、Cygwin、Git Bash 或 PowerShell 中查看报告充其量只能算是模糊的。如果你知道你在寻找什么,你可以破译的结果,但最好是查看 HTML 报告或通过'平原'选项与
$ cover report plain。
它是如何工作的
测试代码覆盖率很重要,因为它可以突出应用中不必要或未充分使用的代码。在这个解决方案中,您看到了如何利用 Node 覆盖框架来查看您的代码覆盖率。
node-cover 框架是全局安装的,允许您在您的终端或 shell 中使用 cover 方法,无论您的 Node.js 应用驻留在哪里,也无论您希望在哪里运行覆盖率测试。要运行测试,使用命令$ cover run <file>。
运行之后,您现在拥有了对代码覆盖率的引用。要查看该访问,您需要通过命令$ cover results [type] [file]查看结果。类型可以是“cli”,这是默认设置,并尝试在您的终端中生成 ASCII 图形布局。这在 Mac 或 Linux 上工作得很好,但是对于 Windows 用户来说,命令行界面(CLI)的输出几乎是无法阅读的。幸运的是,还有其他三种输出选择。第一个是 plain,它只输出代码中没有覆盖的一行或多行。对于更多的图形报告,您可以使用“html”选项,这将生成一个完整的报告,可以在浏览器中查看。或者,如果您希望管理自己的实现,或者希望在自定义报告服务中查看数据,您可以为报告选择 JSON 输出类型。JSON 输出包含了报告的所有相关细节,以及执行覆盖率测试的代码的原始源代码。
11-9.伊斯坦布尔报告代码覆盖率
问题
您希望利用伊斯坦布尔代码覆盖框架来分析代码的覆盖范围。
解决办法
有几个库可用于报告 Node.js 应用中的代码覆盖率。对于此解决方案,您将研究伊斯坦布尔。
在本节中,您将创建一个简单的 Node.js 应用,它将有目的地测试您的代码覆盖率,并有助于突出 Node.js 覆盖率库的优势。这个示例代码如清单 11-26 中的所示。
清单 11-26 。代码覆盖示例应用
/**
* coverage tests
*/
function add(a, b, callback) {
callback(a + b);
}
function subtract(a, b, callback) {
callback(a - b);
}
add(2, 3, function(result) {
console.log(result);
});
//subtract(3, 2, function(result) {
// console.log(result);
// });
您将用来显示这组代码的代码覆盖率的代码覆盖率模块是伊斯坦布尔。伊斯坦布尔是全球安装使用$ npm install –g istanbul。然后你可以为你的模块运行覆盖测试,如清单 11-27 所示。
清单 11-27 。伊斯坦布尔覆盖测试
$ istanbul cover 11-5-1.js
5
=============================================================================
Writing coverage object [c:\Users\cgackenheimer\Dropbox\book\code\11\coverage\coverage.json]
Writing coverage reports at [c:\Users\cgackenheimer\Dropbox\book\code\11\coverage]
=============================================================================
=============================== Coverage summary ===============================
Statements : 83.33% ( 5/6 )
Branches : 100% ( 0/0 )
Functions : 66.67% ( 2/3 )
Lines : 83.33% ( 5/6 )
================================================================================
伊斯坦布尔将自动为您生成一个 HTML 报告。它将把它放在 coverage 目录中一个名为 lcov-report 的目录中。上述覆盖率测试的结果可以在浏览器中查看。汇总页面如图 11-3 中的所示,详细覆盖范围如图 11-4 中的所示。
图 11-3 。伊斯坦布尔封面摘要
图 11-4 。伊斯坦布尔封面细节
它是如何工作的
您利用伊斯坦布尔作为一个模块来生成代码覆盖测试。伊斯坦布尔也是全球安装的,以便在整个项目中使用命令行。要用伊斯坦布尔生成覆盖测试,只需运行$ istanbul cover <file> 。这将为您运行测试覆盖率。
当您运行伊斯坦布尔的覆盖测试时,您也可以自动得到报告。没有像 node-cover 那样的辅助步骤来生成测试结果。运行cover命令还将直接在您的 CLI 中生成一个清晰的覆盖率摘要。它还会产生一个 JSON 输出和 HTML。正如你在图 11-4 中看到的,HTML 中的详细概述将突出显示没有用红色标出的陈述。未涵盖的功能以橙色突出显示。
将这些报告添加到您的测试工作流中对于分析您的编码非常有帮助,尤其是当您向您的实现中添加大型模块的时候。如果你已经在重构的过程中,你可能会看到在你的代码覆盖中有大量的缺口。这为下一次迭代提供了一种更明智的重构代码的方式。
11-10.构建完整的测试套件
问题
您希望在您的应用中包含一组测试,以便构建一个健壮的测试套件。
解决办法
在这个解决方案中,您将使用 Mocha 测试框架构建一套要执行的测试。为此,将所有测试构建到测试目录中。对于这个例子,你可以利用前面章节的所有测试,把它们分成几个文件(见清单 11-28 和 11-29 )。
清单 11-28 。第一个测试/Mocha.js 文件
/**
* mocha
*/
var assert = require('assert');
var three = 3;
describe('11-2', function() {
describe('equal', function() {
it('should return true that 3 equals "3"', function() {
assert.equal('three', '3', '3 equals "3"');
});
});
describe("strictEqual", function() {
it('"3" only strictly equals 3.toString()', function() {
assert.strictEqual('3', three.toString(), '3 and "3" are not strictly equal');
});
});
describe("notEqual", function() {
it('should be that 3 is not equal to three', function() {
assert.notEqual(three, 'three', '3 not equals three');
});
});
describe("ok", function() {
it('should return that false is not truthy', function() {
assert.ok(false, 'not truthy ');
});
});
describe("ok", function() {
it('should be true that true is truthy', function() {
assert.ok(true, 'truthy');
});
});
});
清单 11-29 。第二次测试/Mocha.2.js
/**
* mocha
*/
var three = 3;
var should = require('should');
var square = function(a) {
if (typeof a !== 'number') return false;
return a * a;
};
describe('11-3 sync', function() {
describe('square a number', function() {
it('should do this syncronously', function() {
square(three).should.equal(9);
});
it('should fail when the target is not a number', function() {
square('three').should.be.false;
});
});
});
var squareAsync = function(a, cb) {
result = a * a;
cb(result);
};
describe('11-4 async', function() {
describe('square a number', function() {
it('should perform async', function() {
squareAsync(three, function(result) {
result.should.equal(9);
});
});
it('should fail', function() {
squareAsync('three', function(result) {
result.should.not.be.a('number');
});
});
});
});
它是如何工作的
这是一个简单的测试套件,但是它展示了像 Mocha 这样的工具的威力,它允许您利用一个命令来执行整个测试文件夹。
当您执行mocha命令时,它将查找位于测试目录中的文件,并对每个文件执行 Mocha 测试。mocha命令将只在为至少一个 Mocha 测试格式化的文件上执行。这意味着,一个包含许多只使用 Node.js assert 方法测试代码的文件的目录,仍然只对包含 Mocha 测试的文件运行测试。不要求整个文件只包含摩卡测试;事实上,为了让 Mocha 命令行找到文件并执行测试,将结果添加到聚合总数中,文件中只需要出现一个mocha describe。
11-11.在您的工作流程中实施测试
问题
在开发 Node.js 应用时,您希望将测试作为工作流的一部分来实现。
解决办法
将测试添加到您的工作流程中可以通过多种方式完成。重要的是,您能够将测试添加到您的过程中。实现测试的一种方法是,每次为 Node.js 应用创建新代码时,简单地调用您选择的测试框架。这可以是一个手动过程,或者,正如您将在下一节中看到的,是一个自动化任务。
实现测试的另一种方法是在 package.json 文件的脚本部分添加一个测试命令,如清单 11-30 所示。
清单 11-30 。npm 测试的 Package.json
{
"name": "Ch11",
"version": "0.0.0",
"description": "chapter 11",
"main": "mocha.js",
"directories": {
"test": "test"
},
"dependencies": {
"chai": "∼1.7.2",
"nodeunit": "∼0.8.1",
"should": "∼1.2.2",
"vows": "∼0.7.0"
},
"devDependencies": {
"mocha": "∼1.12.0"
},
"scripts": {
"test": "mocha"
},
"author": "cgack"
}
在这里,每当您想要运行您的测试时,您可以简单地调用$ npm test。这允许您对每个想要测试的模块进行统一的测试调用,而不管底层的测试基础设施如何。
它是如何工作的
你在本章前面已经看到了如何使用你选择的框架来执行测试,所以在你的工作流程中实现它是简单的,就像调用框架一样。
添加使用npm test测试模块的能力是一个有用的技巧。这是因为 npm 命令行能够解析您的测试目录,并使用您为描述 Node.js 模块的 npm package.json 文件提供的测试脚本。
在工作流中实现测试的方法有很多,但是,正如您将在下一节中看到的,自动化测试是确保您不会错过测试代码机会的最简单的方法。
11-12.自动化您的测试
问题
您需要自动化您的测试,这样您就可以始终确保您的 Node.js 在每次对代码进行更改时都得到测试并通过。
解决办法
有许多方法可以自动化您的测试解决方案。如果你正在使用摩卡,最简单的方法之一就是使用摩卡手表功能。为此,调用 Mocha,如清单 11-31 所示。
清单 11-31 。摩卡手表
$ mocha -w
............
11 passing (9 ms)
1 failing
1) calc tests simple maths should be easy to subtract:
AssertionError: four minus 2 is two
at Context.<anonymous> (c:\Users\cgackenheimer\Dropbox\book\code\11\test\calc_test.js:14:11)
at Test.Runnable.run (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runnable.js:211:32)
at Runner.runTest (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:355:10)
at c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:401:12
at next (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:281:14)
at c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:290:7
at next (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:234:23)
at Object._onImmediate (c:\Users\cgackenheimer\AppData\Roaming\npm\node_modules\mocha\lib\runner.js:258:5)
at processImmediate [as _immediateCallback] (timers.js:330:15)
............
12 passing (16 ms)
\ watching
另一种测试自动化方法是利用 Grunt.js 任务运行器来驱动测试。与 Grunt 更流行的集成之一是 grunt-contrib-nodeunit 模块。这允许您用一个命令将所有的 Node 单元测试分组并一起运行。
要配置 Grunt nodeunit,您需要安装带有$ npm install –g grunt-cli的 Grunt 命令行。然后,您将通过输入$ npm install grunt-contrib-nodeunit来安装grunt-contrib-nodeunit模块。接下来,您需要在 package.json 文件中添加对 Grunt 和 grunt-contrib-nodeunit 的引用。安装完这些依赖项后,将 Grunt 安装到名为$ npm install grunt –save-dev的项目中。
现在您需要创建一个 Gruntfile.js 文件,该文件将导出您希望 Grunt 执行的任务。一个简单的 Grunt 文件用于您之前创建的 nodeunit 测试,看起来类似于清单 11-32 中的例子。
清单 11-32 。Node 单元的 Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
nodeunit: {
all: ['nodeunit.js']
}
});
grunt.loadNpmTasks('grunt-contrib-nodeunit');
};
您现在可以通过运行grunt nodeunit命令来执行任何 Node 单元测试,如清单 11-33 所示。
清单 11-33 。正在执行 grunt Node 单元
$ grunt nodeunit
Running "nodeunit:all" (nodeunit) task
Testing nodeunit.js......OK
>> 8 assertions passed (7ms)
Done, without errors.
它是如何工作的
使用 Mocha 命令行的内置文件监视器来监视文件,最初会运行一次测试,然后监视目录中的文件是否有任何更改。
在这个解决方案中,您首先创建了一个 calc.js 模块(参见清单 11-34 ),该模块错误地将一个乘法运算符放在了减法方法中减法运算符所在的位置。
清单 11-34 。错误的 calc.js 模块
/**
* calc
*/
var calc = module.exports = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a * b;
}
};
您在您编写的测试中调用了mocha –w,watcher,期望减法能够工作,但是他们在减法测试中失败了。观察目录中的文件是否有变化。然后,您需要修改文件,以从 a 中减去 b,文件监视器会发现这一变化。一旦文件发生变化,Mocha 再次运行测试——这次通过了——并继续观察文件的变化。拥有这种对测试的持续反馈是非常有益的,它可以帮助激励你首先编写测试。
接下来,您看到了如何通过将这些测试自动化到 Grunt.js 任务运行器框架来将 Node 单元测试集成到您的测试工作流中。这里的关键是您能够设计您的 gruntfile.js,以便您能够覆盖一个目录中的所有测试或者适合某个文件结构的测试。在您的案例中,您提供了一个文件;然而,nodeunit 的 Grunt 任务配置可以接受各种命名的测试,不仅仅是这里看到的“all ”,还可以将它们分解成子模块。nodeunit 配置还允许实现通配符,因此您可以使用' test/*来定位测试目录中的所有文件。js '或类似的模式。
让测试成为 Node.js 开发过程中自然和自动化的一部分是至关重要的,尤其是在项目规模增长的时候。您已经看到了自动实现测试的两种方法,随着项目规模的增长,这减轻了运行测试的负担。