Typescript 与 Mongoose 的最佳实践

前言

mongoose 是 node.js 里操作 mongodb 的最好的 orm 工具。
typescript 则是带了 type 的 js 超集。
在开发过程中经常会碰到写错字段名的问题,只有到了运行阶段才能发现(或许也没发现。。。),使用 typescript 可以达到以下目的:

  1. 智能提示,不会输错字段名啦,当然这个取决于你的 IDE 是否支持 typescript。
  2. 类型检查,错误的类型定义可以在编译期发现。

接下来就看看这两个东西怎么配合吧。

完整的 Express + Typescript + Mongoose 的 Demo 可以参考之前的文章 :使用 typescript 做后端应用开发

安装准备

  1. typescript
    typescript 的安装配置这里不赘述,可以看 typescript 的官方文档。

  2. mongoose

下载 mongoose 的类型定义文件

$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev

typescript 的 tsd 的组织方式一直在变化,目前的方式算是比较简单的,typescript 官方把常见的库 Definition 都放在 npm 上了。

Model 的定义

以一个 User Model 为例。

先定义一个接口

src/interfaces/User.ts:

export interface IUser {
createdAt?: Date;
email?: string;
firstName?: string;
lastName?: string;
}

Schema + Model

src/model/User.ts:

import {Document, Schema, Model, model} from "mongoose";
import {IUser} from "../interfaces/User";

export interface IUserModel extends IUser, Document {
fullName(): string;
}

export const UserSchema: Schema = new Schema({
createdAt: Date,
email: String,
firstName: {type: String, required: true},
lastName: {type: String, required: true}
});
UserSchema.pre("save", function (next) {
let now = new Date();
if (!this.createdAt) {
this.createdAt = now;
}
next();
});
UserSchema.methods.fullName = function (): string {
return (this.firstName.trim() + " " + this.lastName.trim());
};

export const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

Mongoose 的 tsd 中是使用泛型来实现对具体 Model 的定义的,我们自己定义的 Model 是 IUser(包含数据库字段),Mongoose 提供的基础的 Model 定义是 Document(包括 find findOne 等操作) ,继承这个两个接口就是最后的 Model 啦。

有个比较尴尬的地方是:Mongoose 的 Schema 定义和 IUser 的定义是非常相似却又不是同一个东西,你需要写两遍属性定义。而且应该会经常改了一处忘了另一处。

这里提供一个解决方案是:用代码生成器根据 Schema 来生成 Interface 。
只提供思路,不给实现。

RUN 起来

引入 User Model。
src/app.js:

import * as mongoose from 'mongoose'
// mongoose.Promise = global.Promise;
import {User} from './model/User'
mongoose.connect('mongodb://localhost:57017/test');

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', async function () {
console.log('we're connected!');
try {
await User.remove({})
const user = new User({firstName: 'feng', lastName: 'nick', email: 'jfeng@kalengo.com'})
await user.save()
const fuser = await User.findOne({})
console.log('email', user.email);
console.log('email err', user.email2);
console.log('email', fuser.email);
console.log('fullName', fuser.fullName());
console.log('lastName', fuser.lastName);
console.log('createdAt', fuser.createdAt);
} catch (err) {
console.log('err ', err.stack);
}
});

一切正常。

得到了什么

智能提示

IDE 会根据 IUser 的结构来智能提示,终于不用怕很长的字段名啦

类型检查

属性不存在

类型不匹配

OK, 这样就实现文章开头提到的目的啦。

源码

https://github.com/myfjdthink/typescript-mongoose

参考文章

TypeScript: Declaring Mongoose Schema + Model
mongoose 文档

Node 后端开发的几种测试方式

后端开发中,只需要针对接口(无 GUI)做自动化测试,还是比较容易实现的,所以在开发过程中尽量写好测试。在这里总结下我在工作中用到的测试方法。这里以 Nodejs 的后端开发为例。

单元测试

单元测试的重要性不言而喻,算是业界共识了。
在有完善的单元测试的基础上,可以很方便地做到持续集成。
提到单元测试,大家很容易想到 TDD,我个人认为 TDD 并不是一件很容易做到的事情,特别是小团队需求多变的时候,往往会追求先出功能后补测试。

集成测试

考拉里的集成测试一般是通过一个接口对 N 个方法进行集成,这个接口会调用 N 个方法。这种集成测试是对单元测试的一种补充。
因为集成测试的粒度比较大,在做测试的时候往往会碰到个很麻烦的问题:怎么准备测试数据?

准备测试数据

测试数据存储位置
使用 js 来存储数据,相对于 json 来说 js 可以编程,例如需要定义一个日期是昨天,json 是做不到的。

在测试开始前载入测试数据

准备测试数据是件比较繁琐的事情,建议直接从生产环境拉取用户数据。脱敏后作为测试数据用。

写个脚本去线上拉取数据

GenTextUserData.test.js
关键点:
mongodump with query
tosource

平行测试

重构用,新写一个方法,新旧方法一起执行,判断结果是否一致。把不一致的结果收集起来。
效果卓群,可以发现很多意想不到的 BUG。
但是比较使用范围有限,只能测试只读的方法。

灰度测试

最实用,成本很低的方案。
灰度发布,只有部分人可以使用新功能,观察一段时间。

全量覆盖测试

使用大量真实数据来测试,真实数据可以提供足够的样本。
例如开发一个新功能,把所有用户的数据 load 进来,模拟使用这个新功能,看看是否有报错。
但是这个做法成本毕竟高,只有重要的功能才需要这样做。
而且用户数据大于百万级别时,整个测试耗时将非常漫长,可以考虑过滤部分用户把量控制在10万以内。

人工测试

在测试环境,作为用户,体验一遍此次开发的功能,看看前面的测试是否有遗漏。
辅助工具
HDC
POSTMAN

flow type 体验

flow type 是由 Facebook 出品的类型检查工具。
最近在一个新的后端项目上体验flow,也在内部做了分享,在这里再次总结下 flow 的优缺点。
优点:
  • 类型检验,能提前发现一些问题
  • 相对于 typescript ,flow 像 babel 一样提供了一个 runtime,可以直接运行flow 代码
还欠缺的地方:
  • webstorm 对 flow 支持并不完善,不能做到像 typescript 那样做到代码补全,而且 webstorm 执行 flow 检查会卡顿
  • 和 mongoose 无法有效配合,目前 flow 对第三方库的支持甚少
  • 对 commonjs 支持太差,require 第三方包会报错,暂时未找到解决方案,估计是我没用对。