美文网首页饥人谷技术博客
Travis CI 上的 Express x ORM 单元测试

Travis CI 上的 Express x ORM 单元测试

作者: 写代码的海怪 | 来源:发表于2020-04-17 16:33 被阅读0次

今天写后端的时候遇到一个问题,就是怎么在 Traivs CI 上搞 ORM 单元测试。搜了一圈,发现中文说这个的博客很少,那就在这里分享一下我的经历吧。

背景

这里先说说背景:使用 Express 搭建一个 login 的服务,后端会查询数据库,然后返回相应的结果。

我要做的有两件事:

  1. 本地单元测试 login 功能
  2. 部署到 Travis CI 上

再说下我所用到的技术:

  1. Express.js 后端框架
  2. sequelize, sequelize-typescript 一个 ORM 框架,后者只是 TypeScript 的支持库
  3. jest, ts-jest, babel-jest 单元测试框架,后面两个是 TypeScript 和 Babel 的支持工具
  4. supertest 测试请求的框架

OK,不多废话,我们就开始吧。

login 服务

这是我的 auth route 文件,这里定义了一个 login 的服务。

import express from 'express'
import UserModel from '@/models/UserModel'

// '/auth'
const AuthRouter = express.Router()

// 登录
AuthRouter.post('/login', async (req, res) => {
  const {email, password} = req.body
  const user = await UserModel.findOne({
    where: {email}
  })

  if (!user) {
    res.status(401)
    return res.json({
      message: '用户不存在'
    })
  }

  if (user.password !== password) {
    res.status(401)
    return res.json({
      message: '密码不正确'
    })
  }

  return res.json(Mock.mock({
    token: 'OK'
  }))
})

export default AuthRouter

这里的代码很好理解:从 req.body 中拿 emailpassword,使用 UserModel 去取对应的 user,处理用户不存在、密码不正确两个错误。如果没错误则返回 token。(这里的 token 应该是 JWT Token,为了演示就用 'OK' 表示)

UserModel

这里你可能注意到了 UserModel,那我们当然要定义一下这个 model 了。

import {DataTypes} from 'sequelize'
import {Column, Table, Model} from 'sequelize-typescript'

@Table({tableName: 'users'})
class UserModel extends Model<UserModel> {
  @Column(DataTypes.STRING)
  public password!: string

  @Unique
  @Column(DataTypes.STRING)
  public email!: string
}

export default UserModel

没看过 sequelize 也没关系,这里就是定义了一张 users 表,字段有 passwordemail。通过 findOne 就可以做 query 查到对应的 user 了。

sequelize 实例

定义完了 model,就要创建数据库连接了,这里 sequelize 提供的方法很简单,也很容易理解。

import {Sequelize} from 'sequelize-typescript'
import consola from 'consola'
import UserModel from '@/models/UserModel'
import {parseEnv} from '@/utils/config'

// 创建连接实例
const initDB = () => {
  const {DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD} = process.env

  if (!DB_HOST || !DB_PORT || !DB_NAME || !DB_USER) {
    throw new Error('环境变量不存在')
  }

  const sequelize = new Sequelize({
    database: DB_NAME,
    dialect: 'mysql',
    host: DB_HOST,
    port: parseInt(DB_PORT),
    username: DB_USER,
    password: DB_PASSWORD,
    storage: ':memory:',
    models: [UserModel],
    logging: msg => console.log(msg),
    define: {
      charset: 'utf8'
    }
  })

  // Test connection
  sequelize.authenticate()
    .then(() => consola.success('成功连接数据库'))
    .catch((error) => consola.error('无法连接数据库: ', error))

  return sequelize
}

// 开始读入 Model
parseEnv()
const db = initDB()

export default db

这里要画一下重点:
1. 不去检查 DB_PASSWORD,在 if (!DB_HOST || !DB_PORT || !DB_NAME || !DB_USER) 这里没有对 DB_PASSWORD 做检测,这是因为,我们数据库有可能不设置密码,那么 DB_PASSWORD 应该是等于 undefined 的,所以不需要检测。
2. define: {charset: 'utf8'} 这是一定要定义 utf8,因为有些环境如 Travis CI 中,MySQL 可能不是 utf8,会报错Incorrect string value: '\xF0\x90\x8D\x83\xF0\x90...' for xxx 'content' at row 1,有乱码的问题。
3. DB_PORT 得到的是 string 类型,要用 parseInt 转成 number 类型。
4. 这里使用了 parseEnv 函数,功能是解析本地下的 .env 文件,当然在像 Travis CI 或者 Heroku 上你是可以在系统中指定 env 变量,但是本地的时候还是需要去解析 .env 文件的。

.env

这里给出 .env 文件。

DB_HOST=127.0.0.1
DB_NAME=test
DB_PORT=3306
DB_USER=root
DB_PASSWORD=45678

parseEnv 函数如下。

import path from 'path'
import dotenv from 'dotenv'

// 解析 .env
export const parseEnv = () => {
  const {NODE_ENV} = process.env
  if (NODE_ENV !== 'development' && NODE_ENV !== 'dev-test') return

  const envPath = path.resolve(__dirname, '../.env')
  const result = dotenv.config({
    path: envPath
  })

  if (result.error) {
    throw result.error
  }
}

这里要说明一下,只有在 developmentdev-test 环境下才需要去解析。因为 production 环境可以在控制台直接指定 env 变量,如


而持续集成环境中也可以在控制台直接指定 env 变量,如

所以,在 package.json 中,不仅要添加 test 还要添加 dev-test,后者用于本地跑测试。

  "scripts": {
    "test": "jest",
    "dev-test": "NODE_ENV=dev-test jest"
  }

配置 Jest

上面说的都是我们的业务代码,这里开始说测试,首先,我们装完了 jest, ts-jestbabel-jest 后,要定义 jest.config.js 文件。

module.exports = {
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/$1'
    },
    moduleFileExtensions: [
        'ts',
        'js',
        'json'
    ],
    transform: {
        "^.+\\.ts$": "ts-jest",
        '^.+\\.js$': 'babel-jest',
    },
    collectCoverage: true,
    collectCoverageFrom: [
        '<rootDir>/routes/**/*.ts',
        "!**/node_modules/**"
    ],
    coverageReporters: ["html", "text-summary", "lcov"],
}

只需要关注 collectCoverageFrom,我们要测试 /routes 下的文件,不测试 node_modules 下的文件。

测试用例

下面我们来写一个简单的测试用例吧。

import request from 'supertest'
import db from '@/models/db'
import app from '@/app'
import {initMockDB} from '../../../mocks/dbObjects'

describe('auth', () => {
  beforeAll(async () => {
    await db.sync({force: true})
    await initMockDB()
  })
  afterAll(async () => {
    await db.close()
  })
  describe('/login', () => {
    it('成功登录', async () => {
      const res: request.Response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'user1@mail.com',
          password: '123456'
        })
      expect(res.status).toEqual(200)
      expect(res.body).toHaveProperty('token')
    })
  })
})

这里的 db 指的就是 sequelize 创建出来的实例,你也可以理解成数据库连接。

beforeAll 这里做的是强制清除原来的数据库,再按 UserModel 去创建 users 表。initMockDB 就是用 sequelize api 去创建一个用户,这里理解一下就好了,很简单,不放代码了。

afterAll 这里做的是要关闭这个数据库连接。否则会出现报错:test case 还没完全跑完,jest 会一直在等待。

describe 里应该容易理解,就是发个 login 请求,判断 status 和 res.body 是否存在 token。

运行 yarn dev-test 应该会显示成功执行测试。

注意:这里有个小坑,如果遇到了下面找不到测试用例的情况:


可以尝试跑 yarn dev-test --no-cache,这是因为在生成 coverage report 的时候会出现 map 不成功的问题。可以看这里
了解更多。

Travis CI

搞完了本地,当然要搞 Travis CI 了,不为什么,就为了可以在 Readme 上放个既帅气又装逼的 build|passing 小标。这里先给个 .travis.yml 文件。

language: node_js

node_js:
  - node

cache:
  yarn: true

services:
  - mysql

before_script:
  - mysql -e 'create database test;'

services 里注明要安装一个 MySQL,然后 before_scripts 里我们创建一个 test 数据库,注意,这个数据库是没有密码的(官网说了

除了要配置 .travis.yml 文件,我们还要在 Travis CI 网站上配置 env 变量。

因为 MySQL 数据库是没有密码的,所以就不需要配置 DB_PASSWORD 了。

最后一步就是 git commit -m '终于写了一个单元测试' 然后 git push 就 OK 了。

相关文章

网友评论

    本文标题:Travis CI 上的 Express x ORM 单元测试

    本文链接:https://www.haomeiwen.com/subject/jtryvhtx.html