写测试

每当我们生成一个钩子或服务时, 生成器也会设置一个基本的 Mocha test, 我们可以使用它来为它实现单元测试. 在本章中, 我们将为我们的 处理数据创建服务 的集成测试实现单元测试.

我们可以运行 code Linter 和Mocha测试

npm test

这将最初失败, 因为我们在钩子中实现了标准测试未涵盖的功能. 所以让我们先通过.

单元测试挂钩

测试单个钩子的最好方法是设置一个虚拟Feathers应用程序, 其中包含一些返回我们期望的数据并可以测试的服务, 然后注册钩子并进行实际的服务调用以验证它们返回我们期望的内容.

我们创建的第一个钩子是用于处理新消息. 对于这个钩子, 我们可以创建一个 messages 虚拟自定义 服务, 只返回来自 create 服务方法的相同数据. 假装我们是经过身份验证的用户, 我们必须通过 params.user. 对于此测试, 这可以是一个带有 _id 的简单JavaScript对象.

test/hooks/process-messages.test.js 更新为以下内容:

const assert = require('assert');
const feathers = require('@feathersjs/feathers');
const processMessage = require('../../src/hooks/process-message');

describe('\'process-message\' hook', () => {
  let app;

  beforeEach(() => {
    // Create a new plain Feathers application
    app = feathers();

    // Register a dummy custom service that just return the
    // message data back
    app.use('/messages', {
      async create(data) {
        return data;
      }
    });

    // Register the `processMessage` hook on that service
    app.service('messages').hooks({
      before: {
        create: processMessage()
      }
    });
  });

  it('processes the message as expected', async () => {
    // A user stub with just an `_id`
    const user = { _id: 'test' };
    // The service method call `params`
    const params = { user };

    // Create a new message with params that contains our user
    const message = await app.service('messages').create({
      text: 'Hi there',
      additional: 'should be removed'
    }, params);

    assert.equal(message.text, 'Hi there');
    // `userId` was set
    assert.equal(message.userId, 'test');
    // `additional` property has been removed
    assert.ok(!message.additional);
  });
});

我们可以采用类似的方法来测试 test/hooks/gravatar.test.js 中的 gravatar 钩子. :

const assert = require('assert');
const feathers = require('@feathersjs/feathers');
const gravatar = require('../../src/hooks/gravatar');

describe('\'gravatar\' hook', () => {
  let app;

  beforeEach(() => {
    app = feathers();

    // A dummy users service for testing
    app.use('/users', {
      async create(data) {
        return data;
      }
    });

    // Add the hook to the dummy service
    app.service('users').hooks({
      before: {
        create: gravatar()
      }
    });
  });

  it('creates a gravatar link from the users email', async () => {
    const user = await app.service('users').create({
      email: 'test@example.com'
    });

    assert.deepEqual(user, {
      email: 'test@example.com',
      avatar: 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60'
    });
  });
});

在上面的测试中, 我们创建了一个虚拟服务. 但有时, 我们需要完整的Feathers服务功能. feathers-memory 是一个有用的 数据库, 支持Feathers查询语法(和分页)但不支持需要数据库服务器. 我们可以将它安装为开发依赖项:

npm install feathers-memory --save-dev

让我们用它来测试 populateUser 钩子, 将 test/hooks/populate-user.test.js 更新为:

const assert = require('assert');
const feathers = require('@feathersjs/feathers');
const memory = require('feathers-memory');
const populateUser = require('../../src/hooks/populate-user');

describe('\'populate-user\' hook', () => {
  let app, user;

  beforeEach(async () => {
    // Database adapter pagination options
    const options = {
      paginate: {
        default: 10,
        max: 25
      }
    };

    app = feathers();

    // Register `users` and `messages` service in-memory
    app.use('/users', memory(options));
    app.use('/messages', memory(options));

    // Add the hook to the dummy service
    app.service('messages').hooks({
      after: populateUser()
    });

    // Create a new user we can use to test with
    user = await app.service('users').create({
      email: 'test@user.com'
    });
  });

  it('populates a new message with the user', async () => {
    const message = await app.service('messages').create({
      text: 'A test message',
      // Set `userId` manually (usually done by `process-message` hook)
      userId: user.id
    });

    // Make sure that user got added to the returned message
    assert.deepEqual(message.user, user);
  });
});

如果我们现在运行:

npm test

我们所有的测试都应该通过好极了!

注解

运行测试时会打印一些错误堆栈. 这是正常的, 它们是运行404(未找到)错误测试时的日志条目.

测试数据库设置

在测试数据库功能时, 我们希望确保测试使用不同的数据库. 我们可以通过在 config/test.json 中创建一个具有以下内容的新环境配置来实现这一点:

{
  "nedb": "../test/data"
}

NODE_ENV 设置为 test 时, 这将设置NeDB数据库使用 test/data 作为基目录而不是 data/. 对于其他数据库的连接字符串也可以完成同样的事情.

我们还希望确保在每次测试运行之前清理数据库. 为了在跨平台实现这一点, 首先运行:

npm install shx --save-dev

现在我们可以将 package.jsonscript 部分更新为以下内容:

"scripts": {
  "test": "npm run eslint && npm run mocha",
  "eslint": "eslint src/. test/. --config .eslintrc.json",
  "start": "node src/",
  "clean": "shx rm -rf test/data/",
  "mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
}

在Windows上, mocha 应该是这样的:

npm run clean & SET NODE_ENV=test& mocha test/ --recursive --exit

这将确保在每次测试运行之前删除 test/data 文件夹并正确设置 NODE_ENV.

测试服务

为了测试实际的 messagesusers 服务(所有挂钩连线), 我们可以使用任何REST API测试工具发出请求并验证它们是否返回正确的响应.

但是有一种更快, 更简单和完整的方法. 由于Feathers已经提供(和测试)了我们自己的钩子和服务之上的所有东西, 我们可以直接要求 服务, 并通过设置 params.user 来 “fake” 认证 如上面的钩子测试中所示.

默认情况下, 生成器创建服务测试文件, 例如, test/services/users.test.js, 只测试服务是否存在, 如下所示:

const assert = require('assert');
const app = require('../../src/app');

describe('\'users\' service', () => {
  it('registered the service', () => {
    const service = app.service('users');

    assert.ok(service, 'Registered the service');
  });
});

然后我们可以添加使用该服务的类似测试. 以下是一个更新的 test/services/users.test.js, 它增加了两个测试. 第一个验证是否可以创建用户, 设置gravatar并加密密码. 第二个验证密码未发送到外部请求:

const assert = require('assert');
const app = require('../../src/app');

describe('\'users\' service', () => {
  it('registered the service', () => {
    const service = app.service('users');

    assert.ok(service, 'Registered the service');
  });

  it('creates a user, encrypts password and adds gravatar', async () => {
    const user = await app.service('users').create({
      email: 'test@example.com',
      password: 'secret'
    });

    // Verify Gravatar has been set as we'd expect
    assert.equal(user.avatar, 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60');
    // Makes sure the password got encrypted
    assert.ok(user.password !== 'secret');
  });

  it('removes password for external requests', async () => {
    // Setting `provider` indicates an external request
    const params = { provider: 'rest' };

    const user = await app.service('users').create({
      email: 'test2@example.com',
      password: 'secret'
    }, params);

    // Make sure password has been removed
    assert.ok(!user.password);
  });
});

我们对 test/services/messages.test.js 采取了类似的方法. 我们从 users 服务创建一个特定于测试的用户. 然后我们在创建新消息时将其作为 params.user 传递, 并验证该消息的内容:

const assert = require('assert');
const app = require('../../src/app');

describe('\'messages\' service', () => {
  it('registered the service', () => {
    const service = app.service('messages');

    assert.ok(service, 'Registered the service');
  });

  it('creates and processes message, adds user information', async () => {
    // Create a new user we can use for testing
    const user = await app.service('users').create({
      email: 'messagetest@example.com',
      password: 'supersecret'
    });

    // The messages service call params (with the user we just created)
    const params = { user };
    const message = await app.service('messages').create({
      text: 'a test',
      additional: 'should be removed'
    }, params);

    assert.equal(message.text, 'a test');
    // `userId` should be set to passed users it
    assert.equal(message.userId, user._id);
    // Additional property has been removed
    assert.ok(!message.additional);
    // `user` has been populated
    assert.deepEqual(message.user, user);
  });
});

再次运行 npm test, 验证我们所有钩子的测试以及新的服务测试是否通过.

Client/server 测试

您可以编写测试来启动应用程序的服务器, 以及测试可用于调用服务器的Feathers客户端. 此类测试可能会暴露客户端与服务器之间交互的错误. 它们还可用于测试来自客户端的请求的身份验证. 将其安装为开发依赖项:

npm install @feathersjs/client --save-dev

从上面测试 test/services/users.test.js 在服务器上运行. 我们将其转换为以下 tests/services/client-users.test.js, 因此测试在客户端而不是服务器上运行. 这也会导致客户端身份验证被测试.

const assert = require('assert');
const feathersClient = require('@feathersjs/client');
const io = require('socket.io-client');
const app = require('../../src/app');

const host = app.get('host');
const port = app.get('port');
const email = 'login@example.com';
const password = 'login';

describe('\'users\' service - client', function () {
  this.timeout(10000);
  let server;
  let client;

  before(async () => {
    await app.service('users').create({ email, password });

    server = app.listen(port);
    server.on('listening', async () => {
      // eslint-disable-next-line no-console
      console.log('Feathers application started on http://%s:%d', host, port);
    });

    client = await makeClient(host, port, email, password);
  });

  after(() => {
    client.logout();
    server.close();
  });

  describe('Run tests using client and server', () => {
    it('registered the service', () => {
      const service = client.service('users');

      assert.ok(service, 'Registered the service');
    });

    it('creates a user, encrypts password and adds gravatar', async () => {
      const user = await client.service('users').create({
        email: 'testclient@example.com',
        password: 'secret'
      });

      // Verify Gravatar has been set to what we'd expect
      assert.equal(user.avatar, 'https://s.gravatar.com/avatar/1b9c869fa7a93e59463c31a377fe0cf6?s=60');
      // Makes sure the password got encrypted
      assert.ok(user.password !== 'secret');
    });

    it('removes password for external requests', async () => {
      // Setting `provider` indicates an external request
      const params = { provider: 'rest' };

      const user = await client.service('users').create({
        email: 'testclient2@example.com',
        password: 'secret'
      }, params);

      // Make sure password has been removed
      assert.ok(!user.password);
    });
  });
});

async function makeClient(host, port, email, password) {
  const client = feathersClient();
  const socket = io(`http://${host}:${port}`, {
    transports: ['websocket'], forceNew: true, reconnection: false, extraHeaders: {}
  });
  client.configure(feathersClient.socketio(socket));
  client.configure(feathersClient.authentication({
    storage: localStorage()
  }));

  await client.authenticate({
    strategy: 'local',
    email,
    password,
  });

  return client;
}

function localStorage () {
  const store = {};

  return {
    setItem (key, value) {
      store[key] = value;
    },
    getItem (key) {
      return store[key];
    },
    removeItem (key) {
      delete store[key];
    }
  };
}

我们首先在 server 上调用以创建新用户. 然后, 我们为我们的应用程序启动服务器. 最后调用函数 makeClient 来创建Feathers客户端并使用新创建的用户对其进行身份验证.

各个测试保持不变, 除了服务调用现在在客户端(client.service(...).create)而不是在服务器上进行(app.service(...).create).

describe('Run tests using client and server', 语句停止为每个测试创建一个新的服务器和客户端.这导致测试模块运行明显更快, 尽管测试现在暴露于潜在的迭代.您可以删除该语句以将测试彼此隔离.

代码覆盖率

代码覆盖率是一种很好的方式, 可以了解我们在测试期间实际执行了多少代码. 使用 Istanbul 我们可以轻松添加它:

npm install nyc --save-dev

现在我们必须更新 package.jsonscript 部分:

"scripts": {
  "test": "npm run eslint && npm run coverage",
  "coverage": "npm run clean && NODE_ENV=test nyc mocha",
  "eslint": "eslint src/. test/. --config .eslintrc.json --fix",
  "start": "node src/",
  "clean": "shx rm -rf test/data/",
  "mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
},

在Windows上, coverage 命令如下所示:

npm run clean & SET NODE_ENV=test& nyc mocha

现在跑:

npm test

这将打印出一些额外的覆盖信息.

更改默认测试目录

要更改默认测试目录, 请在项目的 package.json 文件中指定所需的目录:

{
  "directories": {
    "test": "server/test/"
  }
}

另外, 不要忘记更新 package.json 文件中的mocha脚本:

"scripts": {
  "mocha": "mocha server/test/ --recursive --exit"
}

下一步是什么?

就是这样 - 我们的聊天指南已经完成!我们现在有一个经过全面测试的REST和实时API, 带有一个包含登录和注册的简单JavaScript前端. 关于使用Feathers的完整细节, 或者开始构建自己的第一个Feathers应用程序, 请关注 API