12

我正在使用 Protractor 为我的 Angular 应用程序编写端到端测试。我可以模拟 httpBackend 进行单元测试,但我想实际调用服务器并获取 JSON 响应并再次编写测试返回的数据。
我已经阅读了很多关于 stackoverflow 的内容,但无法理解这是如何完成的。

我使用 $http 吗?如何将它注入我的 Jasmine 测试中?如何将响应 JSON 返回到我的 Jasmine 测试中?

任何帮助或资源链接以及有关执行此操作的说明都会有所帮助。

同样,我不想模拟服务器,我想访问服务器并取回 JSON。

谢谢!

4

4 回答 4

5

我现在正在自己解决这个问题。我认为简短的回答是你设置你的应用程序就像你自己手动测试它一样 - 所以量角器实际上只是一个机器人用户,它没有(嗯,几乎没有)访问你的应用程序的内部。

因此,如果您的应用程序需要一个 Web 服务器(大多数都需要),那么您启动该 Web 服务器,然后让量角器通过浏览器连接到您的应用程序并运行它。

就我而言,我的目标是使用 grunt 在开始运行我的量角器 e2e 测试之前调用一个执行基本数据库设置的任务——这应该给我一个已知的数据库状态。

例如,我一直在编写使用 Rails 4 和 AngularJS 的教程,关于使用量角器进行 e2e 测试的部分不是特定于 Rails 的,可能有用: http ://technpol.wordpress.com/2013/ 11/16/5 端到端测试/

于 2013-11-16T05:46:44.767 回答
3

Protractor 应该用于完整堆栈的端到端测试。

在这种情况下,测试通常会练习 Angular 应用程序(填写表单、按下按钮),这将触发 Angular 应用程序调用 REST 服务器,该服务器返回数据,您的 Angular 应用程序在 DOM 中转换这些数据会发生变化,然后您的端到端-结束测试断言。

这意味着您可能希望在启动 Protractor 之前启动您的应用程序服务器(它托管 Angular 应用程序并且是 REST 后端,我想)

如何做到这一点超出了 Protractor 的范围。

这方面的困难通常是如何设置您的数据库,以便 e2e 测试知道返回您的 JSON 服务时会发生什么。

于 2013-11-10T17:38:09.400 回答
1

下面是一个示例,说明如何仅在 e2e 测试运行时自动启动和停止单独的节点服务器。包含一个简单的快速模拟服务器脚本作为示例 API。

量角器.conf.js

const {SpecReporter} = require('jasmine-spec-reporter');
const forever = require('forever-monitor');

const child = new (forever.Monitor)('index.js', {
  max: 10,
  silent: false,
  args: ["--port", "3001"],
  sourceDir: 'mock-server'
});

let startResolve;
let stopResolve;
const startPromise = new Promise((resolve) => startResolve = resolve);
const stopPromise = new Promise((resolve) => stopResolve = resolve);

child.on('start', function () {
  console.info('Forever started mocks.');
  startResolve();
});

child.on('restart', function () {
  console.info('Forever restarting mocks for ' + child.times + ' time');
});

child.on('exit:code', function (code) {
  if (code) {
    console.info('Forever exit mocks with code ' + code);
  } else {
    console.info('Forever exited mocks.');
  }
  stopResolve();
});

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function () {
    }
  },
  beforeLaunch: function () {
    child.start();

    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });

    return startPromise;
  },
  onPrepare() {
    jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
  },
  onCleanUp() {
    child.stop();

    return stopPromise;
  }
};

模拟服务器/index.js

// npm install --save express
// npm install --save body-parser
// npm install --save minimist

const express = require('express');
const bodyParser = require('body-parser');
const minimist = require('minimist');

const API_DELAY = 0;
const app = express();
app.use(bodyParser.json({limit: '50mb'}));

// Turn on CORS for browser testing.
app.use(function (req, res, next) {
  let accessHeaderInReq = false;
  if (req.headers.origin) {
    res.header('Access-Control-Allow-Origin', req.headers.origin);
    accessHeaderInReq = true;
  }
  if (req.headers['access-control-request-method']) {
    res.header('Access-Control-Allow-Methods', req.headers['access-control-request-method']);
    accessHeaderInReq = true;
  }
  if (req.headers['access-control-request-headers']) {
    res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
    accessHeaderInReq = true;
  }
  if (accessHeaderInReq) {
    res.header('Access-Control-Max-Age', 60 * 60 * 24 * 365);
  }

  // Intercept OPTIONS method for angular preflight checks.
  if (accessHeaderInReq && req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  else {
    next();
  }
});

app.get('/api/foo', function (req, res, next) {
  console.info('GET - returning foo', req.body);
  setTimeout(() => {
    res.json({
      foo: "bar"
    });
  }, API_DELAY);
});

const argv = minimist(process.argv.slice(2));
const port = argv.port || 3000;
console.log("Starting express on port", port);
app.listen(port);

对于持续集成环境,您可以安装模拟服务器 node_modules 而无需更改目录,如下所示:

npm --prefix ./mock-server install ./mock-server
于 2017-08-11T15:31:11.283 回答
0

除了我们的 Protractor e2e 测试之外,我们还进行这种类型的“直接点击 API 并测试响应中的数据”集成测试。对于 API 端测试,您不需要 Protractor,因为不需要启动浏览器来发送 HTTP 请求到服务器。

这是我们所做的:

  • 直接使用 Jasmine 运行我们的 API 集成测试。(您可以安装一个 jasmine npm 包。)这样,我们就可以保持describe()/it()/expect()我们的 Protractor 环境(基于 Jasmine)中熟悉的语法。因此,与其运行量角器来运行测试,不如运行 jasmine,a la:jasmine --config=jasmine.json path/to/tests/*spec.js
  • 使用 request-promise npm 包生成 HTTP 请求。

我们的规范文件看起来像这样:

describe('API Tests written in Jasmine', function() {    
  beforeAll(() => authAsAdmin());

  it('Should get a proposal object as auth\'d user', function() {
    const httpOptions = {
      uri: `/proposals/100`,
    };
    return requestWithAuth(httpOptions)
      .then(res => {
        const proposal = res.body.proposal;
        // console.log(`Proposal ${proposal.id} title: ${proposal.title}`);
        expect(proposal.id).toEqual(100);
        expect(res.statusCode).toEqual(200);
        expect(res.statusMessage).toBe('OK');
      });
  });

我们的规范文件依赖于我们在 Jasmine 帮助文件(Jasmine 如何工作的标准机制的一部分)中设置的一些全局帮助方法,如下所示:

const rp = require('request-promise');
...
// Declare our helper methods globally so they can be accessed anywhere in tests
global.requestWithAuth = requestWithAuth;
global.authAs = authAs;
global.authAsAdmin = () => authAs(ADMIN_USER);
global.catchErrorInLocation = (error, location) => {
  throw new Error(`Error in ${location}\n     ${error}`);
};
global.catchErrorInBeforeAll = (error) => catchErrorInLocation(error, 'beforeAll()');

function authAs(user) {
  ...
}

/**
 * Combines a given set of options with the DEFAULT_HTTP_OPTIONS plus a session token
 * and initiates an http request, returning a promise for the response.
 * @param {Object} options properties matching request-promise API
 * @param {string} token, optional session token. sessionToken used by default.
 * @returns {Promise} request-promise response
 */
function requestWithAuth(options, token = sessionToken) {
  Object.assign(options, { ...DEFAULT_HTTP_OPTIONS, ...options }); // Merge custom options with default options
  options.headers['x-token'] = token; // Merge current session token into options
  options.uri = `${BASE_URL}${options.uri}`; // Update the URI to include the correct base path

  return rp(options);
}

我希望这有帮助。

于 2018-07-20T18:59:56.413 回答