thinkjs / thinkjs

Use full ES2015+ features to develop Node.js applications, Support TypeScript.
https://thinkjs.org/
MIT License
5.31k stars 617 forks source link

think.app.server不存在 #1037

Closed matinjugou closed 6 years ago

matinjugou commented 6 years ago

DESC

按照issue #841 的方法进行测试的时候,想用supertest模拟请求进行API测试,然后发现虽然think.model有内容,但是think.app却没有按照运行流程所说注入think.app.server,也就没办法构造http请求进行测试。以为是异步的问题,但是使用繁忙等待发现think.app.server一直是undefined。另外,按照这样的方式进行测试,travis也出现了报错,如果把require(path.join(process.cwd(), 'production.js'));换成require(path.join(process.cwd(), 'development.js'));,就不会有

failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/manager.js with error: Unexpected token (17:8)

这样的报错,如果直接把production.js里面的代码放进test的文件里(路径深度增加了一层),就不会有这样的报错

Error: Cannot find module '../../base.js' ……

但是会提示think.model undefined 求解到底怎么用supertest进行测试?

ENV

OS Platform: travis.ci

Node.js Version: 9.3.0

ThinkJS Version:3.2.3

code

// your code here
const test = require('ava');
const request = require('supertest');
const path = require('path');
require(path.join(process.cwd(), 'production.js'));

test('eee', t => {
  console.error("think=", think);
  console.error("app=", think.app);
  /*
  request(think.app.server).post('/api/staff/login')
    .set('Content-Type', 'application/json')
    .send({
      staffId: '1_s1',
      password: '1_s2'
    })
    .expect('Content-Type', /json/)
    .expect(200);
  */
  const a = think.model('staff');

  console.error(a);
  t.pass();
});

production.js

const Application = require('thinkjs');

const instance = new Application({
  ROOT_PATH: __dirname,
  proxy: true, // use proxy
  env: 'production'
});

instance.run();

development.js

const Application = require('thinkjs');
const babel = require('think-babel');
const watcher = require('think-watcher');
const notifier = require('node-notifier');

const instance = new Application({
  ROOT_PATH: __dirname,
  watcher: watcher,
  transpiler: [babel, {
    presets: ['think-node']
  }],
  notifier: notifier.notify.bind(notifier),
  env: 'development'
});

instance.run();

error message

// your error message here
4.23s$ npm run test
> thinkjs-application@1.0.0 test /home/travis/build/matinjugou/yogurt-backend
> THINK_UNIT_TEST=1 nyc ava test/ && nyc report --reporter=html
(node:4574) Warning: process.on(SIGPROF) is reserved while debugging
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/manager.js with error: Unexpected token (17:8)
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/note.js with error: Unexpected token (14:8)
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/quickReplyPublic.js with error: Unexpected token (22:8)
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/sessionPair.js with error: Unexpected token (15:8)
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/staff.js with error: Unexpected token (2:8)
failed to instrument /home/travis/build/matinjugou/yogurt-backend/src/model/user.js with error: Unexpected token (6:8)
{ Error: Cannot find module '../../base.js'
    at Function.Module._resolveFilename (module.js:555:15)
    at Function.Module._load (module.js:482:25)
    at Module.require (module.js:604:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/src/logic/api/manager/user/list.js:1:999)
    at Module._compile (module.js:660:30)
    at Module.replacementCompile (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:63:13)
    at module.exports (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/default-require-extensions/js.js:8:9)
    at /home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4
    at extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/require-precompiled/index.js:16:3)
    at /home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4
    at require.extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/ava/lib/process-adapter.js:105:4)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Module.require (module.js:604:17)
    at require (internal/module.js:11:18)
    at exports.interopRequire (/home/travis/build/matinjugou/yogurt-backend/node_modules/think-loader/loader/util.js:14:13)
    at files.forEach.file (/home/travis/build/matinjugou/yogurt-backend/node_modules/think-loader/loader/common.js:16:26)
    at Array.forEach (<anonymous>)
    at Object.loadFiles (/home/travis/build/matinjugou/yogurt-backend/node_modules/think-loader/loader/common.js:12:11)
    at Object.load (/home/travis/build/matinjugou/yogurt-backend/node_modules/think-loader/loader/common.js:64:32)
    at Loader.loadLogic (/home/travis/build/matinjugou/yogurt-backend/node_modules/think-loader/index.js:55:19)
    at thinkLoader.loadData (/home/travis/build/matinjugou/yogurt-backend/node_modules/thinkjs/lib/loader.js:44:37)
    at thinkLoader.loadAll (/home/travis/build/matinjugou/yogurt-backend/node_modules/thinkjs/lib/loader.js:110:12)
    at Application.run (/home/travis/build/matinjugou/yogurt-backend/node_modules/thinkjs/lib/application.js:196:18)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/production.js:2:73)
    at Module._compile (module.js:660:30)
    at Module.replacementCompile (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:63:13)
    at module.exports (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/default-require-extensions/js.js:8:9)
    at /home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4
    at extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/require-precompiled/index.js:16:3)
    at /home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4
    at require.extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/ava/lib/process-adapter.js:105:4)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Module.require (module.js:604:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/test/controller.api.staff.login.js:4:1)
    at Module._compile (module.js:660:30)
    at Module.replacementCompile (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:63:13)
    at extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/require-precompiled/index.js:13:11)
    at /home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4
    at require.extensions.(anonymous function) (/home/travis/build/matinjugou/yogurt-backend/node_modules/ava/lib/process-adapter.js:105:4)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Module.require (module.js:604:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/ava/lib/test-worker.js:33:1)
    at Module._compile (module.js:660:30)
    at Module.replacementCompile (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:63:13)
    at module.exports (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/default-require-extensions/js.js:8:9)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/node_modules/append-transform/index.js:67:4)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Function.Module.runMain (module.js:701:10)
    at Function.runMain (/home/travis/.node-spawn-wrap-4574-10c5e5561d2d/node:40:10)
    at Object.<anonymous> (/home/travis/build/matinjugou/yogurt-backend/node_modules/nyc/bin/wrap.js:20:4)
    at Module._compile (module.js:660:30)
    at Object.Module._extensions..js (module.js:671:10)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Function.Module.runMain (module.js:701:10)
    at Object.<anonymous> (/home/travis/.node-spawn-wrap-4574-10c5e5561d2d/node:140:8)
    at Module._compile (module.js:660:30)
    at Object.Module._extensions..js (module.js:671:10)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Function.Module.runMain (module.js:701:10)
    at startup (bootstrap_node.js:194:16)
    at bootstrap_node.js:618:3 code: 'MODULE_NOT_FOUND' }
think= { app: { subdomainOffset: 2, proxy: true, env: 'production' },
  version: '3.2.3',
  messenger: 
   Messenger {
     domain: null,
     _events: {},
     _eventsCount: 0,
     _maxListeners: undefined },
  Controller: [Function: Controller],
  Logic: [Function: Logic],
  Service: [Function: Service],
  service: [Function],
  beforeStartServer: [Function],
  ROOT_PATH: '/home/travis/build/matinjugou/yogurt-backend',
  APP_PATH: '/home/travis/build/matinjugou/yogurt-backend/src',
  loader: 
   Loader {
     appPath: '/home/travis/build/matinjugou/yogurt-backend/src',
     thinkPath: '/home/travis/build/matinjugou/yogurt-backend/node_modules/thinkjs',
     modules: [] },
  config: [Function],
  logger: 
   Logger {
     _logger: DateFileLogger { [Symbol(_logger)]: [Logger] },
     debug: [Function: bound debug],
     info: [Function: bound info],
     warn: [Function: bound warn],
     error: [Function: bound error] },
  sendEmail: [Function: thinkEmail],
  Model: { [Function: Model] HAS_ONE: 1, HAS_MANY: 2, BELONG_TO: 3, MANY_TO_MANY: 4 },
  model: [Function: injectModel],
  Mongo: 
   { [Function: Mongo]
     ObjectID: 
      { [Function: ObjectID]
        index: 7314453,
        createPk: [Function: createPk],
        createFromTime: [Function: createFromTime],
        createFromHexString: [Function: createFromHexString],
        isValid: [Function: isValid],
        ObjectID: [Circular],
        ObjectId: [Circular] } },
  mongo: [Function: injectModel],
  cache: [Function: thinkCache] }
app= { subdomainOffset: 2, proxy: true, env: 'production' }
Model {
  config: 
   { logConnect: false,
     logSql: false,
     logger: [Function: logger],
     handle: 
      { [Function: Mysql]
        Query: [Function: MysqlQuery],
        Schema: [Function: MysqlSchema],
        Parser: [Function: MysqlParser] },
     database: '',
     prefix: 'think_',
     encoding: 'utf8',
     host: '127.0.0.1',
     port: '',
     user: 'root',
     password: 'root',
     dateStrings: true,
     type: 'mysql' },
  modelName: 'staff',
  options: {},
  _cacheConfig: 
   { type: 'redis',
     file: 
      { timeout: 86400000,
        handle: [Function: FileCache],
        cachePath: '/home/travis/build/matinjugou/yogurt-backend/runtime/cache',
        pathDepth: 1,
        gcInterval: 86400000 },
     redis: 
      { timeout: 86400000,
        handle: [Function: RedisCache],
        port: 6379,
        host: '123.206.22.71',
        password: '' } },
  [Symbol(think-model-relation)]: Relation { model: [Circular], relation: {}, relationName: true },
  [Symbol(think-models)]: 
   { user: [Function: Model],
     staff: [Function: Model],
     sessionPair: [Function: Mongo],
     quickReplyPublic: [Function: Mongo],
     quickReplyPrivate: [Function: Mongo],
     note: [Function: Model],
     manager: [Function: Model],
     index: [Function: Model],
     company: [Function: Model],
     comments: [Function: Model] } }
  ✔ eee
  1 test passed [14:19:04]

more description

// your detail description

matinjugou commented 6 years ago

初步判断如下: ../../base.js问题是组内同学写错了 之所以server不存在是因为在环境变量THINK_UNIT_TEST=1的情况下框架没有runserver,但是去掉该环境变量并使用mocha测试后,由于process.argv[2]不存在,框架会从第四分支使用runInworker启动服务,使用think.app.on('appReady')监听服务后,在travis的持续集成自动化测试中程序会卡死,不知道为什么 testing.js

const Application = require('thinkjs');
const path = require('path');

const instance = new Application({
  ROOT_PATH: __dirname,
  proxy: true, // use proxy
  env: 'testing'
});

instance.run();

测试代码

const assert = require('assert');
const request = require('supertest');
const path = require('path');
require(path.join(process.cwd(), 'testing.js'));

think.app.on('appReady', () => {
  console.error('appReady');
  console.error('server=', think.app.server);
  if (think.app.server !== undefined) {
    describe('staff', function() {
      describe('login', function() {
        it ('server should run and login should failed', function(){
          /*
          request(think.app.server).post('/api/staff/login')
            .set('Content-Type', 'application/json')
            .send({
              staffId: '1_s1',
              password: '1_s2'
            })
            .expect('Content-Type', /json/)
            .expect(200)
            .end(function(err, res) {
              if (err) throw err;
              console.error("res=", res);
            });
           */
          const a = think.model('staff');
          console.error(a);
          assert.equal(1,2);
          done();
        })
      })
    });
  }
});
matinjugou commented 6 years ago

问题已解决,摸索出了一个测试实践

matinjugou commented 6 years ago

博客地址:http://blog.magichc7.com/post/thinkjs3-functiontest.html

方法

首先在项目根目录下创建testing.js的测试环境文件,内容基本复制production.js:

const Application = require('thinkjs');
const path = require('path');

const instance = new Application({
  ROOT_PATH: __dirname,
  proxy: true, // use proxy
  env: 'testing'
});

instance.run();

instance.runInWorker({ port: 2333 }); #添加这两行
module.exports = instance; 

然后在test文件夹下,创建一个测试文件:

const assert = require('assert');
const request = require('supertest');
const path = require('path');
const instance = require(path.join(process.cwd(), 'testing.js'));

describe('Test Title', function() {
  describe('Test subtitle', function() {
    it ('Test description', function(done){
      const f = function() {
        request(think.app.server).post([url])
         .set('Content-Type', 'application/json')
          .send({
          // ...构造请求参数,supertest具体用法参见supertest文档
          })
          .expect(200)
          .end(function(err, res) {
            if (err) throw err;
            // ...测试逻辑
            done();
          });
      };
      setTimeout(f, 4000);
    })
  })
  after(function () {
    process.exit();
  })
});

并添加adapter.testing.js,在其中声明用于测试环境的配置,比如测试数据库地址,写法同adapter.js,根据官方文档,adapter.js中的同名配置会被覆盖。 最后在package.json中加入test命令: linux环境:

"test": "THINK_UNIT_TEST=1 mocha -t 20000" 

windows环境:

"test": " set THINK_UNIT_TEST=true && mocha -t 20000" 

不要忘记安装mocha等相关依赖,在测试前要先npm run compile编译

原理解释

我们要知道为什么这样可以启动一个http server并用于测试服务。


THINK_UNIT_TEST=1如果我们基于原生的koa或者express,那么我们直接用koa()或者express()就能创建出一个http服务。但是按照#841的方法,是不会启动http服务的。原因在于这句环境变量设置。我们追踪testing.js中的instance.run()方法,会定位到thinkjs源码中的lib/application.js文件,会看见根据环境变量与process参数的不同,框架有四条启动分支:

run() {
    if (pm2.isClusterMode) {
      throw new Error('can not use pm2 cluster mode, please change exec_mode to fork');
    }
    // start file watcher
    if (cluster.isMaster) this.startWatcher();

    const instance = new ThinkLoader(this.options);
    const argv = this.parseArgv();
    try {
      console.error(argv);
      if (process.env.THINK_UNIT_TEST) {
        instance.loadAll('worker', true);
      } else if (argv.path) {
        instance.loadAll('worker', true);
        return this.runInCli(argv);
      } else if (cluster.isMaster) {
        instance.loadAll('master');
        return this.runInMaster(argv);
      } else {
        instance.loadAll('worker');
        return this.runInWorker(argv);
      }
    } catch (e) {
      console.error(e);
    }
  }

其中,使用了THINK_UNIT_TEST环境会执行try中的第一分支,第一分支如果进一步追踪会发现框架使用thinkLoader加载了所有的配置等,因此think.model可以正常的实例化模型。但是第一分支没有使用return 语句来真正的以某种模式启动服务。在其他三个分支里,框架使用runInClirunInMasterrunInWorker将服务启动了起来。所以我们需要手动将http服务run起来。

之所以不直接尝试进入其他分支,是因为进入条件不满足。第二第三分支的进入条件是process的argv参数里argv[2]必须存在且为path(第二分支,以命令行方式运行服务)或者port(第三分支)。在平常使用时(npm run start [port]),我们使用node [env].js [port]的方式启动服务,此时port会被正则匹配并解析到argv中,执行第三分支(解析过程具体可以看application.js中的parseArgv()函数)。第二分支我觉得应该是在crontab(定时任务)的场景下使用,被框架自己调用。

而在测试框架中执行时,process的argv[2]中会被注入测试框架自己的参数,比如我们的mocha中,argv[2]='-t',ava中此项会被注入一个json化的对象字符串,但是由于thinkjs没有对path的格式做校验,因此会进入第二分支,并产生错误。


instance.runInWorker({ port: 2333 })使用这句可以手动启动服务,在传入参数中指定port即可。之所以不runInMaster是因为会引发cluster.fork is not a function这个错误,而runInWorker可以直接启动一个工作进程,对于基本的业务逻辑测试已经足够了。这里我们就完成了启动一个http服务。


setTimeout(f, 4000);在测试代码中,我们使用延时任务以等待服务启动的时间。thinkJs提供了'appReady'事件,所以这里可以考虑用think.app.on('appReady')改写。这里如果不做相应的等待,server对象不会被注入到think.app.server中,也就无法使用supertest


process.exit()测试完成后,需要在mocha的after方法里手动关闭测试进程,原因是之前使用runInWorker方法启动的服务仍然在运行。这种方法虽然可以关闭process完成测试,但是Nodejs会残留。不过在travis持续集成下的自动化测试中,最终系统资源都会释放掉。


mocha -t 20000由于mocha默认一个测试应该在两秒内完成,所以需要用-t参数延时以免超时导致测试不通过。

总结

缺点

不能干净的结束掉测试进程,对于本地测试会需要手动关闭node进程

可能的改进思路

在config中,声明一个createServer函数,来自定义启动,以获取supertest所需的server对象。

感谢

@wurining @welefen

lyy666-git commented 4 years ago

npm WARN thinkjs-application@1.0.0 No repository field.请问一下这个错误怎么解决呀

lizheming commented 4 years ago

@lyy666-git 这是一个 warning 忽略即可,具体错误内容是说你的项目没有在 package.json 中配置 git 仓库地址。