随着Node.js的横空处世,原本目的是解决部分后端的问题,谁知道却无心间给前端开发带来了一场颠覆性的革命,今后前端拉开了现代化开发的序幕。现在,做为前端开发,不管是想进阶或是拓宽我的知识边界,node.js早已经是前端必须掌握的了。拿下node.js,你还在犹豫什么?javascript
Koa.js是基于node.js的一个开发框架,小巧灵活,对于一些中小型项目开发仍是比较友好的。Koa上手简单,所以成为了避免少小伙伴上手node开发的选择之一。本文主要从如下几个方面讲解koa后端开发最核心的部份内容,让人人都是全栈小能手:css
开发node后端须要安装node.js环境和npm包管理工具,这块就很少说了,相信如今的前端小伙伴基本都会的。首先建立一个文件文件夹做为咱们的项目目录:html
// 终端。建立koa-test并进入该目录
mkdir koa-test
cd koa-test
复制代码
紧接着,像咱们平时作前端项目同样安装koa库:前端
// 终端:安装koa2
cnpm i koa -S
复制代码
根目录下建立app.js做为咱们程序的入口,就像咱们vue项目中的main.js:vue
// 引入Koa库并初始化
const Koa = require('koa');
const app = new Koa();
// 启动服务
// 监听3000端口,就像vue中默认的是监听8080同样
// 固然了你也能够监听其余端口
const appService = app.listen(3000, () => {
console.log('[Koa]Server is starting at localhost:3000');
});
// 导出服务(是为了供单测使用)
// 即便这里不导出也正常能够跑项目,后面会讲解这里为何须要导出
module.exports = appService;
复制代码
最后在终端启动咱们的程序服务:java
// node是node.js的命令
node app.js
复制代码
以下图,在浏览器输入localhost:3000,就能够看咱们开启的服务了:node
关于接口,相信你们都不陌生。做为前端开发,天天都会和后台人员提供的接口打交道。下面,咱们看如何在Koa开发的服务中,开发供前端使用的接口吧!git
koa-router
这个库,就比如前端vue开发中的vue-router也是前端的路由同样:// 安装路由库
// -S是--save的简写,表示在生成环境中使用
// -D是--save-dev的简写,表示在开发环境中使用
cnpm i koa-router -S
// 对于前端的参数,咱们是须要获取使用的
// get提交的参数咱们能够轻松得到,
// 可是post的数据,咱们须要解析才能使用
// 所以须要安装koa-bodyparser库来处理post的数据
// 终端执行:
cnpm i koa-bodyparser -S
复制代码
出于标准,咱们须要将api相关的内容独立出来。就像咱们vue项目开发中的src下也会分components、pages、api、assets等。这里咱们在根目录下建立api文件夹,用来存放咱们的路由文件,如图:github
koa-test/api/index.js是咱们api模块的出口,modules文件用来存放全部的API模块,例如这里有user相关接口都在user.js中等。具体的内容会放在后面细说。web
下面咱们看如何在app.js中挂载路由和其余中间件:
// koa-test/app.js
// 引入koa-bodyparser用于解析post数据
const bodyParser = require('koa-bodyparser');
// 引入根目录下的api路由
// 即把koa-test/api/index.js暴露出来的路由引入进来
const router = require('./api')
// app.js中挂载koa-bodyparser
// 注意:在路由挂载前先挂载 koa-bodyparser
app.use(bodyParser());
// 挂载路由
// 服务启动后能够在浏览器输入localhost:3000看到提示
app.use(async ctx => ctx.body = '服务启动成功');
app.use(router.routes());
app.use(router.allowedMethods());
// 省略上面的其余代码
复制代码
// 引入koa-router
const Router = require('koa-router');
// 引入modules文件夹下的路由模块
const articleRouter = require('./modules/articles');
// 实例化Router中间件
const router = new Router();
// 注册路由
// 注意该路由模块文件在注册时增长了'/articles前缀
// 即该模块下全部的接口地址都会以/articles做为前缀
router.use('/articles', articleRouter.routes(), articleRouter.allowedMethods())
// 将注册后的路由导出
// 供app.js中的koa挂载
module.exports = router;
复制代码
articles.js
文件:// 仍是须要先导入koa-router
const Router = require('koa-router');
// 实例化router
const router = new Router();
// 注册get方法
// 能够经过ctx.query获取parse后的参数
// 或者经过ctx.queryString获取序列化后的参数
router.get('/list', (ctx, next) => {
ctx.body = {
code: 200,
data: [
{
id: 1,
name: '小明',
sex: 0,
age: 22
}
],
message: 'ok'
};
});
// 注册post方法
// app.js中挂载koa-bodyparse中间件后,
// 能够经过ctx.request.body获取post参数
// eg:这里的data就是前端post时提交的数据
router.post('/update', (ctx, next) => {
let data = ctx.request.body
ctx.body = {
code: 200,
data,
message: 'ok'
};
});
// 将该模块的路由(api接口)暴露出去
// 供api/index.js路由注册
module.exports = router;
复制代码
这里的ctx.body
,就是返回给前端的json数据。
基本的路由编写就到这了,固然了,实际业务开发中还会涉及到put、delete类型等等的接口。基本写法都大同小异,这里附上koa-router的官网文档地址,查看更多的路由编写细节把。
引用网上的定义就是:
REST 指的是一组架构约束条件和原则。 知足这些约束条件和原则的应用程序或设计就是 RESTful
下面看如何定义rest风格的api接口:
// 获取操做使用get:
// 例如:获取所有文章
get /api/articles
// 带搜索条件带获取文章(例如页数、每页条数、文章类型等等)
get /api/articles?page=1=pageSize=50&type=1
// 获取id为12345带单条文章
get /api/articles/12345
// 资源分类,
// eg:获取id为12345的文章的评论
get /api/articles/12345/comments
// 获取id为12345的文章的带搜索条件的评论
get /api/articles/12345/comments?page=1&pageSize=50
// 提交数据使用post类型:
// 建立文章
post /api/articles
// 更新数据使用put类型:
// 例如:更新id为12345的文章内容
put /api/articles/12345
// 删除id为12345的文章
delete /api/articles/12345
复制代码
REST风格的API编写能够参考廖雪峰大大的这篇 编写REST API 文章
在Koa中开发REST风格的API也很简单,koa-router为咱们的ctx对象提供了params对象,能够获取REST风格的API中的参数。很像vue-router中的动态路由有木有?
router.get('/user/:userId', async ctx => {
// 获取动态路由的参数
// 经过koa-router提供的ctx.params对象获取
const id = ctx.params.userId
// 省略其余代码
}
复制代码
咱们在开发接口过程当中,确定须要测试咱们写的接口正不正确,有木有按照预期返回结果。那么怎么访问咱们的接口查看是否正确呢?最简单的确定有那么一款工具完,咱们直接在上面操做就行了,呢~~以下,能够安装postman使用:
这样的话,咱们能够建立接口来访问咱们写的接口服务,还能够携带各类参数,调试起来仍是很是方便的,这个就很少说了,网上搜postman下载就能够了,免费开源的。
OK,上面说完了编写接口,对应的确定须要咱们操做数据库,而后给前端返回数据,例如基本的增删改查呀。下面先看基本mongodb数据的安装吧,这里以Mac OS为列:
这里介绍的是在终端用curl的方式安装的(显示骚骚的~~),其实直接到官网下载mongo的安装包也是同样的:
# 进入 /usr/local
cd /usr/local
# 下载
sudo curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-4.0.9.tgz
# 解压
sudo tar -zxvf mongodb-osx-ssl-x86_64-4.0.9.tgz
# 重命名为 mongodb 目录
sudo mv mongodb-osx-x86_64-4.0.9/ mongodb
// 添加环境变量
export PATH=/usr/local/mongodb/bin:$PATH
// 新建一个数据库存储目录
sudo mkdir -p /data/db
// 启动mongod
sudo mongod
复制代码
mongodb的安装过程很简单,就不赘述了,更多的安装方法能够参考 mongoDb 安装参考地址
mongodb的可视化工具,我这里推荐的是Studio 3T,能够很方便的链接数据库,查看数据库的内容,或者操做数据库等。
最后这里附上Robo3的下载安装地址,安装很简单,就像装个qq同样,不赘述了。
在node中,咱们基本上是使用mongoose链接、操做数据库。首先,咱们须要安装mongoose:
// 安装mongoose
cnpm i mongoose -S
复制代码
然后,在文件根目录下新建databse文件夹,用来专门放置链接和操做数据相关的文件:
database/index.js中,咱们用来写链接数据库的方法,最后将其导出供app.js中链接使用:
// 引入mongoose库
const mongoose = require('mongoose');
// 定义数据库地址的常量
// 更标准的能够新建一个数据配置文件,
// 用来专门存放数据相关的配置,好比帐号密码等等
const DB_ADDRESS = 'mongodb://localhost/koa-test';
mongoose.Promise = global.Promise;
mongoose.set('useCreateIndex', true);
// 简单封装log
const log = console.log.bind(console);
// 定义链接函数
const connect = () => {
// 重连次数
let connectTimes = 0;
// 设置最大重连次数
const MAX_CONNECT_TIMES = 3;
// 断线重连
const reconnectDB = (resolve, reject) => {
if (connectTimes < MAX_CONNECT_TIMES) {
connectTimes++;
mongoose.connect(DB_ADDRESS, connectConfig);
} else {
log('[mongodb] database connect fail!');
reject();
}
}
// 链接数据库
mongoose.connect(DB_ADDRESS, connectConfig);
return new Promise((resolve, reject) => {
// 监听数据库断开,从新链接
mongoose.connection.on('disconnected', () => {
reconnectDB(reject);
});
// 监听数据库链接出错,从新链接
mongoose.connection.on('error', err => {
log(err);
reconnectDB(reject);
});
// 监听链接成功
mongoose.connection.on('open', () => {
log('[mongodb server] database connect success!');
resolve();
});
});
};
// 暴露出去
exports.connect = connect;
// 还须要引入schema,在下面演示
// ……
复制代码
这里主要的做用就是:
(1)经过mongoose.connect()方法链接数据库;
(2)监听disconnected和error事件,进行数据库重连,而且最多重连三次;
(3)返回promise来告知链接成功与否
复制代码
经过以前的文件夹截图能够看出,咱们建立了schema文件夹,是用来存放全部数据库建模相关的内容,其实就是经过schema来对数据库进行操做的。下面看下如何导入咱们全部的schema下的文件的(虽然一个个引入也是能够的,可是咱们是有追求的程序猿~~):
在databse/index.js:
// 引入glob
const glob = require('glob');
// 引入弄的的path方法
// 能够读取、解析、拼接路径等等
const path = require('path');
// 暴露一个initSchemas方法
// 用于导入database/schema文件夹下全部schema
exports.initSchemas = () => {
// 经过glob读取schema文件夹下内容
glob.sync(path.resolve(__dirname, './schema/', '**/*.js')).forEach(require);
}
复制代码
不清楚glob用法的,这里附上glob文档地址;
更多mongoose的内容能够查看mongoose中文文档地址;
path是node提供的一个模块,主要用来处理和路径相关的内容:
// path使用前,仍是须要先导入
const path = require('path');
// join方法能够将全部参数链接起来,返回一个路径
path.join()
// eg:
path.join('a', 'b', 'c', 'd'); // a/b/c/d
path.join(__dirname, '/a', '//b', '///c', 'd'); // /Users/yoreirei/Documents/demo/node-demo/a/b/c/d
path.join(__dirname, 'a', 'b', '../c', 'd'); // /Users/yoreirei/Documents/demo/node-demo/a/c/d
path.join(__dirname, 'a', './b', './c', './d'); // /Users/yoreirei/Documents/demo/node-demo/a/b/c/d
// parse方法将路径解析为一个路径对象
path.parse()
// eg:
path.parse(path1) // { root: '', dir: 'a/b/c', base: 'd', ext: '', name: 'd' }
path.parse(path2) // { root: '/', dir: '/Users/yoreirei/Documents/demo/node-demo/a/b/c', base: 'd', ext: '', name: 'd' }
// format方法将路径对象转换成路径地址
path.format(parse1) // a/b/c/d
复制代码
注意:__dirname
获取的是当前文件模块所在的绝对路径。这个前端小伙伴在vue-cli的entry应该看到过,很熟悉吧。
拓展来一下,OK,咱们继续schema建模。
// database/schema/User.js
// 引入mongoose
const mongoose = require('mongoose');
// 获取mongoose.Schema方法用于建模
const { Schema } = mongoose;
// 生成id
let ObjectId = Schema.Types.ObjectId;
// 建立用户的schema
// 例如建立一个包含用户名、密码、建立时间、
// 最后登陆时间、点赞内容、收藏内容的schema
const userSchema = new Schema({
UserId: ObjectId,
// 咱们能够定义每一个字段的类型,例如String、Number、Array等等
// 能够定义该字段的值是否惟一,若是设置了惟一,
// 那么后续插入相同的值时就会报错
userName: {
unique: true,
type: String
},
password: String,
likes: {
type: Array,
default: []
},
collect: {
type: Array,
default: []
}
}, {
// 加入该配置项,会自动生成建立时间
// 在文档更新时,也会自动更新时间
timestamps: {
createdAt: 'createdAt',
updatedAt: 'updatedAt'
}
});
// 最后,使用mongoose发布模型
mongoose.model('User', userSchema);
复制代码
使用schema建模就是这么简单,小伙伴能够本身扩展建立其余schema。这个操做其实就相似于其余数据中的建表。
基本上咱们的服务中都会涉及到用户的注册和登陆等等。而对于用户注册的密码,咱们是不会明文保存的,这样是不安全的。通常的作法都是对明文密码进行加密后存储,而用户登陆时再对用户的密码加密后后和数据库中加密过的密码进行比对,看是否正确。而前端常见的也能够在用户提交时进行md5等方式的加密提交。
关于加密,咱们可使用bcript对密码的加密与解密。
咱们须要对用户注册时的密码进行加密,使其不可逆。首先,咱们须要安装bcript库:
// 安装bcript
cnpm i bcript -S
复制代码
database/schema/User.js
// 引入bcript
const bcrypt = require('bcrypt');
// 定义bcrip加密时的配置常量
const SALT_ROUNDS = 10;
// 每次保存时进行密码加密
// 注意此处pre的第二个参数,不能是箭头函数,否则拿不到this
userSchema.pre('save', function(next) {
bcrypt.genSalt(SALT_ROUNDS, (err, salt) => {
if (err) return next(err);
bcrypt.hash(this.password, salt, (err, hash) => {
if (err) return next(err);
// 将用户提交的密码替换成加密后的hash
this.password = hash;
next();
});
});
});
复制代码
注意:咱们这里的加密作法时,在用户建模的时候,监听save事件,即用户每次存储数据的时候,都会执行咱们定义的回调,而咱们就在回调的函数中进行加密的操做。
验证用户登陆的密码时,咱们须要拿到用户的密码而后经过bcript验证是否和加密后的数据同样。
database/schema/User.js:
// 定义userSchema的实例方法
// 解密user password
// 注意mehtod要加s
userSchema.methods = {
// 定义一个对比密码是否正确的方法
// userPassword用户提交的密码
// passwordHash数据库查出来的加过密的密码
comparePassword (userPassword, passwordHash) {
return new Promise((resolve, reject) => {
bcrypt.compare(userPassword, passwordHash, (err, res) => {
// 验证完成
// res值为false|true,表示密码不一样/相同
if (!err) return resolve(res);
// 验证出错
return reject(err);
});
});
}
}
复制代码
注意:咱们这里的作法是给schema增长一个实例方法,那么咱们(例如编写登陆接口,那么用户的密码后)经过调用schema的实例去比对密码是否正确。
/** * 用户登陆 * @param { String } userName 用户名 * @param { String } password 密码 */
router.post('/login', async ctx => {
// 前提引入mongoose
// 获取User集合(相似于其余数据的表)
const userModal = mongoose.model('User');
// 集合的实例
const userInstance = new userModal();
// 定义查询参数
const query = { userName: data.userName };
// 先查找用户是否存在
await userModal.findOne(query).exec()
.then(async res => {
// 用户存在,拿到用户数据
// 调用集合的实例方法,比对密码是否正确
// then回调表示验证操做完成
// 经过返回的参数isMatch(true/false)表示验证是否正确
await userInstance.comparePassword(data.password, res.password).then((isMatch) => {
// 验证密码是否正确
if (isMatch) {
// 此处省略token生成,会在后面讲解
// *****
return ctx.body = {
code: 200,
message: 'ok'
};
}
return ctx.body = {
code: 400,
message: '帐号密码错误'
};
}).catch(() => {
return ctx.body = {
code: 500,
message: error.message
};
})
// 用户不存在,直接提示
}).catch(() => {
return ctx.body = {
code: 400,
data: null,
message: '当前用户不存在'
};
});
});
复制代码
关于bcript的内容能够参考 bcript的npm文档地址
其实关于登陆这一块,咱们是须要作登陆鉴权的,好比是否过时等等,再复杂一些还会有redis持久化等等。这里省略了JWT登陆鉴权,下面会介绍。
jwt是经常使用的用户登陆鉴权方式:
(1)前端经过登陆接口拿到token,存到本地,前端在后续的增删改查的时候会在请求头携带token。
(2)后端会根据请求时携带的authorization(即用户token),判断用户是否登陆过时(统一拦截),登陆过时则返回401,或者判断当前用户是否有权限进行此操做。
在koa2中使用jwt,要提到两个中间件:
(2)koa-jwt 拦截(所有/部分)用户请求并验证token
首先安装jsonwebtoken:
// 安装jsonwebtoken
cnpm i jsonwebtoken -S
复制代码
api/modules/user.js中
const { createToken } = require('../../utils/account');
// 根据上面的登陆接口,在用户帐号密码查询正确后
// 生成token返回给前端,createToken方法日后看
const token = createToken(res)
return ctx.body = {
code: 200,
data: token,
message: 'ok'
};
复制代码
根目录下新建utils文件夹,
utils/account.js
// 引入jsonwebtoken
const JWT = require('jsonwebtoken');
// 自定义生成token的密钥(随意定义的字符串)
// 就其安全性而言,不能暴露给前端,否则就能够随意拿到token
const JWT_SECRET = 'system-user-token';
// 生成JWT Token
// 同时能够设置过时时间
exports.createToken = (config = {}, expiresIn = '7 days') => {
const { userName, _id } = config;
const options = { userName, _id };
const custom = { expiresIn };
// 经过配置参数,而后调用JWT.sign方法就会生成token
return JWT.sign(options, JWT_SECRET, custom);
};
// 暴露出密钥
// 这里将密钥暴露出去是为了后面验证的时候会用到
// 为了统一,不用到处写'system-user-token'这个字符串而已
exports.JWT_SECRET = JWT_SECRET;
复制代码
如今完成了登陆接口生成token问题,那么在用户请求的时候,咱们还须要拦截用户请求并验证是否过时。下固然就是koa-jwt
上场了:
首先安装:
// 安装koa-jwt
cnpm i koa-jwt -S
复制代码
app.js中作统一拦截,并设置不须要token的接口(例如登陆、注册等接口):
// 引入jwt
const jwt = require('koa-jwt');
// 拿到咱们的密钥字符串
const { JWT_SECRET } = require('./utils/account');
// jwt验证错误的处理
// jwt会对验证不经过的路由返回401状态码
// 咱们经过koa拦截错误,并对状态码为401的返回无权限的提示
// 注意:须要放在jwt中间件挂载以前
app.use(function(ctx, next){
return next().catch((err) => {
if (401 == err.status) {
ctx.status = 401;
ctx.body = {
code: 401,
message: '暂无权限'
};
} else {
throw err;
}
});
});
// 挂载jwt中间件
// secret参数是用于验证的密钥
// unless方法,设置不须要token的接口
app.use(
jwt({ secret: JWT_SECRET }).unless({
path: [
'/login',
'/register'
]
})
);
复制代码
其实,koa-jwt是封装了koa-less和jsonwebtoken这两个中间件,。
koa-less
的做用是只有在koa-less参数不匹配的时候才执行前面的中间件。 jsonwebtoken
就是上面咱们用的用于生成和解析token的。
看到这,小伙伴是否是想说:
在不少时候咱们须要拿到前端携带的token,从token中获取用户相关的信息,而后再作某些事情。例如,拿到用户的token后,根据token解析出用户的id,而后根据id查询用户存在后再执行某些操做。
api/article.js接口文件,这里的发布文章接口,咱们在拿到用户提交的数据时,根据header中的authorization读取到用户的信息:
// 省略了部分代码
const Router = require('koa-router');
const mongoose = require('mongoose');
// 先导入咱们封装的两个方法
const { decodeToken, parseAuth } = require('../../utils/account');
// 定义文章发布接口
router.post('/article', async ctx => {
let data = ctx.request.body;
// 省略参数验证部分
// ****
// 解析出用户token
const authorization = parseAuth(ctx);
// 根据token解析出token中的用户_id
const tokenDecoded = decodeToken(authorization);
const { _id } = tokenDecoded;
const userModal = mongoose.model('User');
// 先查询用户是否存在,拿到用户信息
await userModal.findById(_id).exec()
// 在用户查到后,再进行建立文章的操做
.then(async res => {
data.author = res.userName
const articleModal = mongoose.model('Article');
const newArticle = articleModal(data);
// 存储文章数据
await newArticle.save()
.then(() => {
return ctx.body = {
code: 200,
message: '保存成功'
};
}).catch(error => {
return ctx.body = {
code: 500,
message: error.message || '保存失败'
};
});
})
.catch(err => {
return ctx.body = {
code: 500,
message: err.message || '用户不存在'
}
})
});
module.exports = router;
复制代码
utils/account.js:
const JWT = require('jsonwebtoken');
// 从ctx中解析authorization
exports.parseAuth = ctx => {
if (!ctx || !ctx.header.authorization) return null;
const parts = ctx.header.authorization.split(' ');
if (parts.length < 2) return null;
return parts[1];
}
// 解析JWT Token
exports.decodeToken = (token) => {
return JWT.decode(token);
};
复制代码
注意: (1)主要思路就是,咱们经过用户请求的header中携带的authorization,解析出用户的_id
,而后经过_id
去数据库查出用户对应的信息(项目大的话,这里会使用例如redis的缓存技术去读取用户的信息,而不是每次直接操做数据库),而后再建立文章。
(2)解析authorization的方法很简单,即便直接调用jsonwebtoken这个库的decode方法便可,相关介绍官网都有说明。
(3)关于怎么拿到authorization这里要说明一点,其实咱们拿到的是下面这种数据,它其实包含了两部分,因此咱们须要解析出咱们想要的内容(即空格后面的内容):
解析方法也很简单,就是根据空格分割,取后面那部分。
上面提到了发布用户数据,那么对于用户提交的数据,若是不作过滤,用户极可能传一些<script>alert('动态注入js')</script>
的恶意代码。
针对这种状况,咱们能够对用户的内容进行过滤,对一些关键字符进行转译。
// 安装xss库
cnpm i xss -S
// 使用,在咱们存文章数据的schema中进行处理
// 在每次存的时候进行过滤
// schema/Article.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const xss = require('xss');
const articleSchema = new Schema({
// 省略部份内容
});
// 每次保存时进行密码加密
// 注意此处pre的第二个参数,不能是箭头函数,否则拿不到this
articleSchema.pre('save', function(next) {
// 对标题和内容进行xss过滤
this.title = xss(this.title);
this.content = xss(this.content);
next();
});
mongoose.model('Article', articleSchema);
复制代码
以下图,能够看到过滤先后的数据库数据对比:
更多关于xss的内容可查看xss 中文文档
注意:若是是mySql等关系型数据库,咱们还会处理防止sql的注入等操做,关于这块,有兴趣的小伙伴能够自行研究研究。
单元测试常见的风格有:行为驱动开发(BDD),测试驱动开发(TDD)。那么二者有什么区别呢?
(1)BDD关注的是整个系统的最终实现是否和用户指望一致。
(2)TDD关注的是取得快速反馈,使全部功能都是可用的。
首先安装:
// 安装
cnpm i mocha -D
复制代码
配置npm测试命令:
// 配置npm script命令
"scripts": {
"test": "mocha"
}
复制代码
新建text/test.js,写测试代码:
// 引入node的断言库
// 其实就像是一个工具库
const assert = require('assert');
// 引入待测试文件
const lib = require('./index');
// 测试iterate这个函数的功能
// describe定义测试的描述
// it定义打印的内容
describe('Math', () => {
describe('#iterate', () => {
it('should return 10', () => {
// 经过断言判断是否经过测试
assert.equal('10', lib.iterate(5, 5));
});
it('should return 0', () => {
assert.equal('0', lib.iterate());
});
it('should return 10', () => {
assert.equal('10', lib.iterate(1, 1));
});
});
});
// test/index.js
const iterate = (...arg) => {
if (!arg.length) return 0;
return arg.reduce((val, cur) => val + cur);
};
module.exports = {
iterate
}
// 终端运行
// 会自动查找test文件夹下全部的测试文件并执行测试
npm test
复制代码
能够看出,当结果不符合预期时会测试不经过:
更多内容能够参考 mocha 文档地址
上面的断言库咱们使用的是node提供的asset断言模块,其实我还有不少其余选择,例如should.js和chai等等,由于这些库提供了比asset更为丰富的供。
这里演示一些should这个断言库:
// 安装should
cnpm i should -D
// 引入
const should = require('should');
// 使用(和上面node的相似)
require('should');
const lib = require('./index');
/** * 单元测试 */
describe('Math', () => {
describe('#iterate', () => {
it('should return 10', () => {
lib.iterate(5, 5).should.be.equal(10);
});
it('should return 0', () => {
lib.iterate().should.be.equal(0);
});
it('should return 10', () => {
lib.iterate(1, 1).should.be.equal(10);
});
});
});
复制代码
should断言库的内容比node的assert断言功能更丰富。断言效果以下:
更多内容能够查看 shuold文档地址
到目前来看,chai其实更为流行一些,chai同时提供了TDD和BDD风格的用法。有兴趣的小伙伴能够翻阅其文档学习查看,基本用法也都大同小异吧。
以上演示都是一些同步测试,那么mocha对异步的测试该怎么作呢?其实很简单,只须要手动调用一个回调函数:
// 待测试文件
// 模拟定义普通的一个异步函数
const asyncFunc = (cb) => {
setTimeout(() => {
console.log('async init after 1000')
cb()
}, 1000)
}
// 测试代码
// PS:省略导入导出的操做了
// 测试普通异步函数
describe('Async', () => {
describe('#asyncFunc', () => {
it('should an async function', done => {
// eg:最关键的是在异步调用完成后,
// 须要手动调用回调函数done,
// 来告诉mocha异步调用完成
asyncTest.asyncFunc(done)
})
})
});
复制代码
效果以下:
// async异步函数的测试
// 模拟一个异步函数:
const getInfo = async (bool) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 若是参数是true,则成功返回
// 不然返回失败
if (bool) return resolve('success');
return reject('fail');
}, 1000);
});
};
// 测试
describe('Async', () => {
describe('#getInfo', () => {
it('should return success', done => {
// 仍是须要手动调用回调函数
// async函数须要写在内部
(async function () {
try {
// 等待异步完成后,手动调用done()
await asyncTest.getInfo(true);
done();
} catch (error) {
done(error)
}
})();
});
});
})
复制代码
效果以下:
若是 await asyncTest.getInfo(true);
参数传入false,模拟测试不经过的效果,会看到下这个样子:
// 效果也是同样的
describe('#getInfo', () => {
it('should return success', async () => {
await asyncTest.getInfo(false);
});
});
复制代码
首先要安装supertest这个库
// 安装
cnpm i supertest -D
复制代码
测试内容:
// 导入咱们的服务
// 前提是在app.js文件中,将启动后的服务导出
//eg: moudle.exports = app.listen(3000)
const app = require('../app');
// 导入用于接口的测试的supertest
const request = require('supertest');
describe('GET /', () => {
it('should return status with 200', (done) => {
// 测试
// get是测试get请求
// expect是指望的内容
request(app)
.get('/')
.expect(200)
.end((err, res) => {
// 在end中获得接口的内容
// 而后根据状况手动调用done
if (err) return done(err);
done();
});
});
});
复制代码
测试结果经过,以下图:
// 文章详情接口须要authorization
// 咱们的测试用例但愿返回200状态码,可是返回了401
// 因此当前测试不经过
describe('GET /artiles/:id', () => {
it('should an article info', (done) => {
request(app)
.get('/api/articles/5d2edc370fddf68b438b6b53')
.expect(200, done);
})
})
复制代码
更多内容能够参考 supertest文档地址
关于单元测试覆盖率,简单提一下,能够测试出咱们的测试代码的覆盖状况是怎样的。
// 首先安装istanbul这个库
cnpm i istanbul -D
复制代码
// 在test文件夹下,打开终端执行如下命令
// 会测试index.js文件的代码测试覆盖状况
istanbul cover index.js
复制代码
如图所示,咱们能够看到8块代码覆盖了3个,2个分支可是一个都没有覆盖到,0个函数,6行代码覆盖了个,以及对应的覆盖率是多少。
同时,在test文件夹下能够看到生成了coverage文件夹,里面保存了覆盖率结果,能够点击index.html查看结果。
index.html能够看的更直观:
更多的内容参考istanbul文档吧。
好了,关于Koa2与mongodb的内容,就到这了,二期初步打算扩展如下关于Koa的内容:
示例代码同步在github,欢迎访问!
百尺竿头、日进一步 我是愣锤,欢迎交流与分享