1. egg.js #

1.1 目录结构 #

├── package.json
├── app.js (app.js 和 agent.js 用于自定义启动时的初始化工作)
├── agent.js (可选)
├── app
|   ├── router.js(用于配置 URL 路由规则)
│   ├── controller(用于解析用户的输入,处理后返回相应的结果)
│   |   └── home.js
│   ├── service (用于编写业务逻辑层,可选)
│   |   └── user.js
│   ├── middleware (用于编写中间件,可选)
│   |   └── response_time.js
│   ├── schedule (用于定时任务,可选)
│   |   └── my_task.js
│   ├── public (用于放置静态资源,可选)
│   |   └── reset.css
│   ├── extend (用于框架的扩展,可选)
│   |   └── application.js app 对象指的是 Koa 的全局应用对象,全局只有一个,在应用启动时被创建。
│       ├── context.js (Context 指的是 Koa 的请求上下文,这是 请求级别 的对象)
│       ├── request.js (Request 对象和 Koa 的 Request 对象相同,是 请求级别 的对象)
│       ├── response.js (Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象)
│       ├── helper.js (Helper 函数用来提供一些实用的 utility 函数)
│   ├── view (用于放置模板文件)
│   |   └── home.tpl
├── |── model (用于放置领域模型)
│   |   └── home.tpl
│   └── extend (用于框架的扩展)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config(用于编写配置文件)
|   ├── plugin.js(用于配置需要加载的插件)
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test(用于单元测试)
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

1.2 访问 #

文件 app ctx service config logger helper
Controller this.app this.ctx this.service this.config this.logger this.app.helper
Service this.app this.ctx this.service this.config this.logger this.app.helper

ctx.helper

2. 初始化项目 #

mkdir egg-news
cd egg-news
npm init -y
npm i egg --save
npm i egg-bin --save-dev
npm i mockjs express morgan egg-mock --save

3. 添加 npm scripts 到 package.json: #

"scripts": {
    "dev": "egg-bin dev"
}

4. 跑通路由 #

├─app
│  │─router.js
│  ├─controller
│  │      news.js
├─config
│      config.default.js
|─package.json

4.1 配置路由 #

app/router.js

module.exports = app => {
    const { router, controller } = app;
    router.get('/news', controller.news.index);
}

4.2 编写控制器 #

app\controller\news.js

const { Controller } = require('egg');
class NewsController extends Controller {
    async index() {
        this.ctx.body = 'hello world';
    }
}
module.exports = NewsController;

4.3 配置文件 #

config\config.default.js

exports.keys = 'zhufeng';

5. 静态文件中间件 #

6. 使用模板引擎 #

├─app
│  │─router.js
│  ├─controller
│  │      news.js
│  ├─public
│  │  ├─css
│  │  │      bootstrap.css
│  │  └─js
│  │         bootstrap.js
│  └─view
│          index.html
├─config
│   config.default.js
│   plugin.js

6.1 安装依赖的插件 #

npm install egg-view-nunjucks --save

6.2 启用插件 #

{ROOT}\config\plugin.js

exports.nunjucks = {
    enable: true,
    package: 'egg-view-nunjucks'
}

6.3 配置模板 #

{ROOT}\config\config.default.js

module.exports=app => {
    let config={};
    config.keys='zhufeng';
    config.view={
        defaultExtension: '.html',
        defaultViewEngine: 'nunjucks',
        mapping: {
            '.html':'nunjucks'
        }
    }
    return config;
}

6.4 编写模板 #

app\view\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/public/css/bootstrap.css">
    <title>新闻列表</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
           {% for item in list%}
                <div class="panel panel-default">
                    <div class="panel-heading">
                       <h3 class="text-center">{{item.title}}</h3>
                    </div>
                    <div class="panel-body">
                        <img src="{{item.image}}" class="img-responsive center-block">
                    </div>
                    <div class="panel-footer">
                        <h3 class="text-center">创建时间: {{item.createAt}}</h3>
                    </div>
                 </div>
            {% endfor %}
        </div>
    </div>
</div>
</body>
</html>

6.5 编写控制器 #

app\controller\news.js

const {Controller}=require('egg');
class NewsController extends Controller{
    async index() {
        const {ctx}=this;
        const list=[
            {
                id: '45154322_0',
                title: '世界首富早晚是这个人,坐拥7家独角兽公司,估值破数万!',
                url: 'http://tech.ifeng.com/a/20180904/45154322_0.shtml',
                image:'http://p0.ifengimg.com/pmop/2018/0905/CFFF918B94D561D2A61FB434ADA81589E8972025_size41_w640_h479.jpeg',
                createAt:new Date().toLocaleString()
            },
            {
                id: '16491630_0',
                title: '支付宝们来了!将来人民币会消失吗?',
                url: 'http://finance.ifeng.com/a/20180907/16491630_0.shtml',
                image:'http://p0.ifengimg.com/pmop/2018/0907/2AF684C2EC49B7E3C17FCB13D6DEEF08401D4567_size27_w530_h369.jpeg',
                createAt:new Date().toLocaleString()
            },
            {
                id: '2451982',
                title: '《福布斯》专访贝索斯:无业务边界的亚马逊 令对手生畏的CEO',
                url: 'https://www.jiemian.com/article/2451982.html',
                image:'https://img1.jiemian.com/101/original/20180907/153628523948814900_a580x330.jpg',
                createAt:new Date().toLocaleString()
            }
        ];
        await ctx.render('index',{list});
    }
}
module.exports=NewsController;

7. 读取远程接口服务 #

在实际应用中,Controller 一般不会自己产出数据,也不会包含复杂的逻辑,复杂的过程应抽象为业务逻辑层 Service。

7.1 添加配置 #

config.default.js

config.news={
       pageSize:10,
    newsListUrl:'http://localhost:3000/news'
}

mock.js

let Mock = require('mockjs');
let express = require('express');
let logger = require('morgan');
let app = express();
app.use(logger('dev'));
app.get('/news', function (req, res) {
    let result = Mock.mock(
        {
            "data|10": [{
                "id": "@id",
                "title": "@csentence",
                "url": "@url",
                "image": "@image(600X500)",
                "createAt": "@datetime",
            }]
        });
    res.json(result);
});
app.get('/cache', function (req, res) {
    res.json({ title: '新闻标题' + Date.now() });
});
app.listen(3000, () => { console.log('mock server is running at port 3000') });

7.2 编写Service #

app/service/news.js

const {Service}=require('egg');
class NewsService extends Service {
    async list(pageNum,pageSize) {
        const {ctx}=this;
        const {newsListUrl}=this.config.news;
        const result=await ctx.curl(newsListUrl,{
            method: 'GET',
            data: {
                pageNum,pageSize
            },
            dataType:'json'
        });
        return result.data.data;
    }
}
module.exports=NewsService;

7.3 编写控制层 #

app/controller/news.js

const {Controller}=require('egg');
class NewsController extends Controller{
    async index() {
        const {ctx,service}=this;
        let {pageNum=1,pageSize=this.config.news.pageSize}=ctx.query;
        const list=await service.news.list(pageNum,pageSize);
        await ctx.render('index',{list});
    }
}
module.exports=NewsController;

8. 计划任务 #

我们还会有许多场景需要执行一些定时任务,例如:

8.1 编写定时任务 #

8.1.1 update_cache.js #

app\schedule\update_cache.js

const { Subscription } = require('egg');
class UpdateCache extends Subscription {
    // 通过 schedule 属性来设置定时任务的执行间隔等配置
    static get schedule() {
        return {
            interval: '1m', // 1 分钟间隔
            type: 'all', // 指定所有的 worker 都需要执行
        };
    }

    // subscribe 是真正定时任务执行时被运行的函数
    async subscribe() {
        console.log('subscribe');
        const res = await this.ctx.curl(this.config.cache.url, {
            dataType: 'json',
        });
        this.ctx.app.cache = res.data;
    }
}

module.exports = UpdateCache;
8.1.1.1 类型 #
8.1.1.2 执行日志 #

8.1.2 config.default.js #

config\config.default.js

+config.cache = {
+   url: 'http://localhost:3000/cache',
+}

mock.js

app.get('/cache',function(req,res){
 res.json({title:'新闻列表'+Date.now()});
});

8.1.3 news.js #

app\controller\news.js

const {Controller} = require('egg');
class NewsController extends Controller{
    async index(){
        const {ctx,service}=this;
        let {pageNum=1,pageSize=this.config.news.pageSize}=ctx.query;
        const list=await service.news.list(pageNum,pageSize);
+       await ctx.render('index',{list,title:this.app.cache?this.app.cache.title:'新闻列表'});
    }
}
module.exports = NewsController;

8.1.4 view\index.html #

app\view\index.html

<div class="container">
+   <h3>{{title}}</h3>
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
           {% for item in list%}
                <div class="panel panel-default">
                    <div class="panel-heading">
                       <h3 class="text-center">{{item.title}}</h3>
                    </div>
                    <div class="panel-body">
                        <img src="{{item.image}}" class="img-responsive center-block">
                    </div>
                    <div class="panel-footer">
                        <h3 class="text-center">创建时间: {{item.createAt}}</h3>
                    </div>
                 </div>
            {% endfor %}
        </div>
    </div>
</div>

8.2 启动时执行定时任务 #

8.2.1 app.js #

9. MySQL #

9.1 安装与配置 #

npm i --save egg-mysql

9.2 开启插件 #

config/plugin.js

exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};

9.3 建表 #

CREATE TABLE `news`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `createAt` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;

INSERT INTO `news` VALUES (1, '世界首富早晚是这个人,坐拥7家独角兽公司,估值破数万!', 'http://tech.ifeng.com/a/20180904/45154322_0.shtml', 'http://p0.ifengimg.com/pmop/2018/0905/CFFF918B94D561D2A61FB434ADA81589E8972025_size41_w640_h479.jpeg', '2019-06-08 22:07:29');
INSERT INTO `news` VALUES (2, '支付宝们来了!将来人民币会消失吗?', 'http://finance.ifeng.com/a/20180907/16491630_0.shtml', 'http://p0.ifengimg.com/pmop/2018/0907/2AF684C2EC49B7E3C17FCB13D6DEEF08401D4567_size27_w530_h369.jpeg', '2019-06-08 22:08:24');
INSERT INTO `news` VALUES (3, '《福布斯》专访贝索斯:无业务边界的亚马逊 令对手生畏的CEO', 'https://www.jiemian.com/article/2451982.html', 'https://img1.jiemian.com/101/original/20180907/153628523948814900_a580x330.jpg', '2019-06-08 22:17:16');

9.4 配置数据源 #

config/config.${env}.js

config.mysql = {
  // 单数据库信息配置
  client: {
    // host
    host: 'localhost',
    // 端口号
    port: '3306',
    // 用户名
    user: 'root',
    // 密码
    password: 'root',
    // 数据库名
    database: 'cms'
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};

9.5 news.js #

app\service\news.js

const {Service}=require('egg');
class NewsService extends Service {
    async list(pageNum,pageSize) {
        const {ctx}=this;
        let result  = await this.app.mysql.query('select * from news');
        return result;
    }
}
module.exports=NewsService;

10. Sequelize #

10.1 安装 #

$ npm install --save egg-sequelize mysql2

10.2 启用插件 #

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
};

10.3 sequelize 配置 #

config/config.default.js

config.sequelize = {
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    username: "root",
    password: "root",
    database: 'cms-development'
};

config/config.test.js

module.exports=app => {
    let config={};
    config.sequelize = {
        dialect: 'mysql',
        host: 'localhost',
        port: 3306,
        username: "root",
        password: "root",
        database: 'cms-test',
    };
    return config;
}

10.4 初始化数据库 #

10.4.1 安装sequelize-cli #

npm install --save sequelize sequelize-cli

10.4.2 .sequelizerc #

const path = require('path');
module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

10.4.3 初始化 Migrations #

npx sequelize init:config
npx sequelize init:migrations

config.json

{
  "development": {
    "username": "root",
    "password": "root",
    "database": "cms-development",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "operatorsAliases": false
  },
  "test": {
    "username": "root",
    "password": "root",
    "database": "cms-test",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "operatorsAliases": false
  }
}

10.4.4 创建users表 #

10.4.4.1 创建目录 #
10.4.4.2 创建升级脚本 #

database\migrations\20190608143311-init-users.js

module.exports = {
  // 在执行数据库升级时调用的函数,创建 users 表
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING } = Sequelize;
    await queryInterface.createTable('users', {
      id: { type: INTEGER, primaryKey: true, autoIncrement: true },
      name: STRING(30),
      age: INTEGER,
      created_at: DATE,
      updated_at: DATE,
    });
  },
  // 在执行数据库降级时调用的函数,删除 users 表
  down: async queryInterface => {
    await queryInterface.dropTable('users');
  },
};
10.4.4.3 执行 migrate 进行数据库变更 #
# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all
10.4.4.4 添加种子数据 #
sequelize seed:create --name init-users
npx sequelize db:seed:all
npx sequelize db:seed:all --env development

database\seeders\20190803152323-init-users.js

module.exports = {
    up: (queryInterface, Sequelize) => {
        return queryInterface.bulkInsert('users', [{
            name: 'zhufeng',
            age:1,
            created_at: new Date(),
            updated_at: new Date()
        },{
            name: 'jiagou',
            age:2,
            created_at: new Date(),
            updated_at: new Date()
        }],{});
    },

    down: (queryInterface, Sequelize) => {
        return queryInterface.bulkDelete('users', null, {});
    }
};

10.4.5 使用 #

10.4.5.1 app\model\user.js #

app\model\user.js

module.exports = app => {
  const { STRING, INTEGER, DATE } = app.Sequelize;

  const User = app.model.define('User', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    name: STRING(30),
    age: INTEGER,
    created_at: DATE,
    updated_at: DATE,
  });

  return User;
};

这个 Model 就可以在 Controller 和 Service 中通过 app.model.User 或者 ctx.model.User 访问到了

10.4.5.2 router.js #

app\router.js

module.exports = app => {
    const { router, controller } = app;
    router.get('/news', controller.news.index);
    router.get('/users', controller.users.index);
}
10.4.5.3 users.js #

app\controller\users.js

const { Controller } = require('egg');
class UserController extends Controller {
    async index() {
       const {ctx,service}=this;
       ctx.body = await ctx.model.User.findAll();
    }
}
module.exports = UserController;

10.4.6 单元测试 #

10.4.6.1 在测试环境中建表 #
npx sequelize db:migrate --env test
10.4.6.2 factory-girl #
npm install --save-dev factory-girl
10.4.6.3 factories.js #

test/factories.js

const { factory } = require('factory-girl');

module.exports = app => {
  // 可以通过 app.factory 访问 factory 实例
  app.factory = factory;
  // 定义 user 和默认数据
  factory.define('user', app.model.User, {
    name: factory.sequence('User.name', n => `name_${n}`),
    age: 18,
  });
};
10.4.6.4 .setup.js #

test/.setup.js

const { app } = require('egg-mock/bootstrap');
const factories = require('./factories');

before(() => factories(app));
afterEach(async () => {
  await Promise.all([
    app.model.User.destroy({ truncate: true, force: true }),
  ]);
});
10.4.6.5 users.test.js #

test/app/controller/users.test.js

// test/app/controller/users.test.js
const { assert, app } = require('egg-mock/bootstrap');

describe('test/app/controller/users.test.js', () => {
  describe('GET /users', () => {
    it('should work', async () => {
      // 通过 factory-girl 快速创建 user 对象到数据库中
      await app.factory.createMany('user', 3);
      const res = await app.httpRequest().get('/users');
      assert(res.status === 200);
      assert(res.body.length === 3);
      assert(res.body[0].name);
      assert(res.body[0].age);
    });
  });
});
10.4.6.6 package.json #
"scripts": {
    "dev": "egg-bin dev",
+    "test": "egg-bin test"
}
npm run test

11. 国际化(I18n) #

11.1 默认语言 #

// config/config.default.js

exports.i18n = {
  defaultLocale: 'zh-CN',
};

11.2 多语言文件 #

多种语言的配置是独立的,统一存放在 config/locale/*.js 下。

11.2.1 en-US.js #

config/locale/en-US.js

module.exports = {
    Email: 'Email',
    'Welcome back, %s!': 'welcome back,%s!',
    'Hello {0}! My name is {1}.': '你好 {0}! 我的名字叫 {1}。',
};

11.2.2 zh-CN.js #

config/locale/zh-CN.js

module.exports = {
    Email: '邮箱',
    'Welcome back, %s!': '欢迎回来,%s!',
    'Hello {0}! My name is {1}.': '你好 {0}! 我的名字叫 {1}.',
};

11.3 获取多语言文本 #

ctx.__('Email')
// zh-CN => 邮箱
// en-US => Email

app\router.js

router.get('/hello', controller.news.hello);

app\controller\news.js

const { Controller } = require('egg');
class NewsController extends Controller {
      async index() {
        const { ctx, service } = this;
        let { pageNum = 1, pageSize = this.config.news.pageSize } = ctx.query;
        const list = await service.news.list(pageNum, pageSize);
+       await ctx.render('index', { list, name: 'zhufeng', names: ['zhufeng', 'jiagou'] });
    }
    async hello(){
        const {ctx}=this;
+        let email = ctx.__('Email');
+        let welcome = ctx.__('Welcome back, %s!', 'zhufeng');
+        let Hello = ctx.__('Hello {0}! My name is {1}.', ['zhufeng','jiagou']);
+        ctx.body = email+welcome+Hello;
    }
}
module.exports = NewsController;

app\view\index.html

 {{__('Email')}} 
 {{__('Welcome back, %s!', name)}} 
 {{__('Hello {0}! My name is {1}.', names)}} 

12. 扩展工具方法 #

app\extend\helper.js

const moment=require('moment');
moment.locale('zh-cn');
exports.fromNow=dateTime => moment(new Date(dateTime)).fromNow();

app\controller\news.js

class NewsController extends Controller {
    async index() {
        const { ctx, service } = this;
        let { pageNum = 1, pageSize = this.config.news.pageSize } = ctx.query;
        const list = await service.news.list(pageNum, pageSize);
+       list.forEach(item => {
+            item.createAt = ctx.helper.fromNow(item.createAt);
+        });
        await ctx.render('index', { list, name: 'zhufeng', names: ['zhufeng', 'jiagou'] });
    }
}

或者 app\view\index.html

<div class="panel-footer">
+    <h3 class="text-center">创建时间: {{helper.fromNow(item.createAt)}}</h3>
</div>

13. 中间件 #

app/middleware/robot.js

module.exports=(options,app) => {
    return async function(ctx,next) {
        const source=ctx.get('user-agent')||'';
        const matched=options.ua.some(ua => ua.test(source));
        if (matched) {
            ctx.status=403;
            ctx.body='你没有访问权限';
        } else {
            await next();
        }
    }
}

config.default.js

    config.middleware=[
        'robot'
    ]
    config.robot={
        ua: [
            /Chrome/
        ]
    }
curl -v  --user-agent 'Chrome'  http://127.0.0.1:7001/news
curl -v  --user-agent 'Chrome'  http://127.0.0.1:7001/news

14.运行环境 #

框架有两种方式指定运行环境:

EGG_SERVER_ENV 说明
local 本地开发环境
prod 生产环境
npm install  cross-env --save-dev
"scripts": {
    "dev": "cross-env EGG_SERVER_ENV=local  egg-bin dev",
    "debug": "egg-bin debug"
}

15. 单元测试 #

15.1 单元测试的优点 #

15.2 测试框架 #

15.3 测试约定 #

15.3.1 测试目录结构 #

15.3.2 测试运行工具 #

统一使用 egg-bin 来运行测试脚本, 自动将内置的 Mocha、co-mocha、power-assert,nyc 等模块组合引入到测试脚本中, 让我们聚焦精力在编写测试代码上,而不是纠结选择那些测试周边工具和模块。

  "scripts": {
    "test": "egg-bin test",
    "cov": "egg-bin cov"
  }

15.3.3 mock #

npm i egg-mock -D

15.3.4 app #

在测试运行之前,我们首先要创建应用的一个 app 实例, 通过它来访问需要被测试的 Controller、Middleware、Service 等应用层代码。

// test/controller/news.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/controller/news.test.js', () => {

});

15.3.5 钩子函数 #

test/order.test.js

describe('egg test', () => {
  before(() => console.log('order 1'));
  before(() => console.log('order 2'));
  after(() => console.log('order 6'));
  beforeEach(() => console.log('order 3'));
  afterEach(() => console.log('order 5'));
  it('should worker', () => console.log('order 4'));
});

15.3.6 ctx #

test/controller/news.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/news.test.js', () => {
  it('should get a ctx', () => {
    const ctx=app.mockContext({
          session: {
            user:{name:'zhufeng'}
        }
    });
    assert(ctx.method === 'GET');
    assert(ctx.url==='/');
    assert(ctx.session.user.name == 'zhufeng');
  });
});

15.3.7 异步测试 #

test/controller/news.test.js

  /*
  it('promise',() => {
    return app.httpRequest().get('/news').expect(200);
  });
  */
  /*
  it('callback',(done) => {
      app.httpRequest().get('/news').expect(200,done);
  });
  */
  /*
  it('async',async () => {
      await app.httpRequest().get('/news').expect(200);
  });
  */

15.4 全面测试 #

15.4.1 测试Controller #

test/controller/user.test.js

15.4.1.1 准备数据 #

app/router.js

router.get('/add',controller.user.add);
router.post('/doAdd',controller.user.doAdd);

app/controller/user.js

const {Controller}=require('egg');
let users=[];
class UserController extends Controller{
    async index() {
        let {ctx}=this;
        await ctx.render('user/list',{users});
    }
    async add() {
        let {ctx}=this;
        await ctx.render('user/add',{});
    }
    async doAdd() {
        let {ctx}=this;
        let user=ctx.request.body;
        user.id=users.length>0?users[users.length-1].id+1:1;
        users.push(user);
        ctx.body = user;
    }
}
module.exports=UserController;
15.4.1.2 user.test.js #

test/controller/user.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
it('test post',async () => {
        let user={username: 'zhufeng'};
        app.mockCsrf();
        let response=await app.httpRequest().post('/doAdd').send(user).expect(200);
        assert(response.body.id == 1);
});

15.4.2 测试service #

test/service/user.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
const {app,assert}=require('egg-mock/bootstrap');
describe('test/service/news.test.js',() => {
    it('newsService',async () => {
        let ctx=app.mockContext();
        let result=await ctx.service.news.list(1,5);
        assert(result.length == 3);
    });
});

15.4.3 Extend 测试 #

应用可以对 Application、Request、Response、Context 和 Helper 进行扩展。 我们可以对扩展的方法或者属性针对性的编写单元测试。

15.4.3.1 application #

egg-mock 创建 app 的时候,已经将 Application 的扩展自动加载到 app 实例了, 直接使用这个 app 实例访问扩展的属性和方法即可进行测试。

app/extend/application.js

let cacheData={};
exports.cache={
    get(key) {
        return cacheData[key];
    },
    set(key,val) {
        cacheData[key]=val;
    }
}

test/app/extend/cache.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/cache.test.js', () => {
    it('cache',async () => {
        app.cache.set('name','zhufeng');
        assert(app.cache.get('name') == 'zhufeng');
  });
});
15.4.3.2 context #

app\extend\context.js

exports.language=function () {
    return this.get('accept-language');
}

test/app/extend/context.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/context.test.js',() => {
    let language="zh-cn";
    it('test language',async () => {
        const ctx=app.mockContext({headers: {'Accept-Language':language}});
        //console.log('ctx.lan',ctx.lan())
        assert(ctx.language() == language);
  });
});
15.4.3.3 Request #

通过 ctx.request 来访问 Request 扩展的属性和方法,直接即可进行测试。 app\extend\request.js

module.exports={
    get isChrome() {
        const userAgent=this.get('User-Agent').toLowerCase();
        return userAgent.includes('chrome');
    }
}

test\extend\request.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/request.test.js',() => {
    it('cache',async () => {
        const ctx=app.mockContext({
            headers: {
                'User-Agent':'I love Chrome'
            }
        });
        assert(ctx.request.isChrome);
  });
});
15.4.3.4 response #

Response 测试与 Request 完全一致。 通过 ctx.response 来访问 Response 扩展的属性和方法,直接即可进行测试。 app\extend\response.js

module.exports = {
  get isSuccess() {
    return this.status === 200;
  },
};

test\extend\response.test.js

describe('isSuccess()', () => {
  it('should true', () => {
    const ctx = app.mockContext();
    ctx.status = 200;
    assert(ctx.response.isSuccess === true);
  });

  it('should false', () => {
    const ctx = app.mockContext();
    ctx.status = 404;
    assert(ctx.response.isSuccess === false);
  });
});
15.4.3.5 Helper #

app\extend\helper.js

module.exports = {
  money(val) {
    const lang = this.ctx.get('accept-language');
    if (lang.includes('zh-cn')) {
      return `¥ ${val}`;
    }
    return `$ ${val}`;
  },
};

test\extend\helper.test.js

describe('money()', () => {
  it('should RMB', () => {
    const ctx = app.mockContext({
      // 模拟 ctx 的 headers
      headers: {
        'Accept-Language': 'zh-cn',
      },
    });
    assert(ctx.helper.money(100) === '¥ 100');
  });

  it('should US Dolar', () => {
    const ctx = app.mockContext();
    assert(ctx.helper.money(100) === '$ 100');
  });
});

15.4.4 测试计划任务 #

update_cache.test.js

const mock = require('egg-mock');
const assert = require('assert');

it('should schedule work fine', async () => {
  const app = mock.app();
  await app.ready();
  await app.runSchedule('update_cache');
  assert(app.cache);
});

16. 布署 #

16.1 安装工具 #

$ npm i egg-scripts --save

16.2 添加npm scripts #

添加 npm scripts 到 package.json:

{
  "scripts": {
    "start": "egg-scripts start --daemon",
    "stop": "egg-scripts stop"
  }
}

17.使用 VSCode 进行调试 #

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Egg",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceRoot}",
      "runtimeExecutable": "npm",
      "windows": { "runtimeExecutable": "npm.cmd" },
      "runtimeArgs": [ "run", "debug" ],
      "console": "integratedTerminal",
      "protocol": "auto",
      "restart": true,
      "port": 9229,
      "autoAttachChildProcesses": true
    }
  ]
}

18.参考 #