赛普拉斯天线_赛普拉斯graphql响应模拟

赛普拉斯天线

If you use Cypress and GraphQL in your project, you may want to test workflows that require mocking calls to GraphQL. There is no native support in Cypress for managing a GraphQL mock in an efficient way. Therefore, we need to create something custom. I will show a solution that I adapted from comments on this GitHub issue.

如果您在项目中使用Cypress和GraphQL,则可能需要测试需要对GraphQL进行模拟调用的工作流。 赛普拉斯不存在以有效方式管理GraphQL模拟的本机支持。 因此,我们需要创建一些自定义项。 我将展示一个解决方案,该解决方案是我根据有关GitHub问题的评论改编而成的。

定义我的标准 (Defining my Criteria)

When I first started, I wasn’t sure exactly how I wanted to configure GraphQL mocking. I knew I wanted to satisfy the following criteria:

刚开始时,我不确定要如何配置GraphQL模拟。 我知道我想满足以下条件:

  • Abstract the GraphQL Mock so test configuration was minimized and leverage Cypress functionality.

    提取GraphQL Mock,以便最小化测试配置并利用赛普拉斯功能。
  • Allow the mock to be as specific or generic as a developer would like.

    允许模拟像开发人员一样具体或通用。
  • Properly track the order of calls to GraphQL to ensure reproducibility.

    正确跟踪对GraphQL的调用顺序,以确保可重复性。

赛普拉斯命令 (Cypress Commands)

Cypress allows you to customize its cy object. You can override functions that exist or add new ones. At Paperless Post, we have several existing commands, such as a custom login function that mocks a user’s session cookie and JWT and another that triggers a hover state. We also have a shortcut to perform a click after x ms. To add support for GraphQL queries, we will create two new command functions.

赛普拉斯允许您自定义其cy对象。 您可以覆盖现有功能或添加新功能。 在Paperless Post,我们有几个现有的命令,例如一个自定义登录功能,该功能模拟用户的会话cookie和JWT,而另一个则触发悬停状态。 我们还有一个快捷方式,可以在x ms之后执行一次单击。 为了增加对GraphQL查询的支持,我们将创建两个新的command函数。

If you don’t already have support for commands, you can enable it by creating a folder called support in your cypress directory. Then create an index.js and put your commands in it or create separate files for each of your commands and import them. More info on commands is available in the Cypress documentation: https://docs.cypress.io/api/cypress-api/custom-commands.html

如果您尚不支持命令,则可以通过在cypress目录中创建一个名为support的文件夹来启用它。 然后创建一个index.js并将您的命令放入其中,或者为每个命令创建单独的文件并将其导入。 有关命令的更多信息,请参见赛普拉斯文档: https : //docs.cypress.io/api/cypress-api/custom-commands.html

The code that adds GraphQL support looks like this:

添加GraphQL支持的代码如下所示:

const GRAPHQL_URL = '/graphql';Cypress.Commands.add('mockGraphQLServer', () => {
// defined in this file. see next section of this blog post.
resetGraphQLMock();
cy.on('window:before:load', win => {
const originalFunction = win.fetch;
function fetch(path, { body, method }) {
if (path.includes(GRAPHQL_URL) && method === 'POST') {
// defined in this file. see next section of this blog post.
return processGraphQLResponse(body);
}
return originalFunction.apply(this, arguments);
}
cy.stub(win, 'fetch', fetch).as('graphqlStub');
});
});

This snippet does the following:

此代码段执行以下操作:

  1. Resets the GraphQL Mock requests tracker and response map.

    重置GraphQL Mock请求跟踪器和响应图。
  2. Before the window loads execute the following.

    在加载窗口之前,请执行以下操作。
  3. Store the existing fetch for default behavior if not a GraphQL request.

    如果不是GraphQL请求,则将现有的fetch存储为默认行为。

  4. Create a new fetch method that checks if the request is for GraphQL and calls a handler we’ll create to process the quest. Otherwise, use the original fetch.

    创建一个新的获取方法,该方法将检查请求是否针对GraphQL,并调用我们将创建的处理程序来处理任务。 否则,请使用原始获取。
  5. Then overwrite the window’s fetch function with the newly created one and alias to graphqlStub. This alias is different than the ones used with cy.route.

    然后用新创建的窗口的访graphqlStub函数以及graphqlStub别名覆盖窗口的访graphqlStub 。 该别名不同于cy.route所使用的cy.route

If you aren’t 100% migrated to GraphQL, you can still mock other calls.

如果您不是100%迁移到GraphQL,您仍然可以模拟其他调用。

Caveat: These calls will not show up in the Network tab when running your cypress tests because we are not executing the calls using the polyfill fetch that you can configure in cypress.json using "experimentalFetchPolyfill": true. However, your implementation may be different. We are using Apollo Boost as our GQL client.

警告:在运行cypress测试时,这些调用不会显示在“网络”选项卡中,因为我们没有使用可以在cypress.json使用"experimentalFetchPolyfill": true配置的cypress.json执行调用"experimentalFetchPolyfill": true 但是,您的实现可能有所不同。 我们正在使用Apollo Boost作为我们的GQL客户。

添加GraphQL模拟处理程序函数 (Add the GraphQL mock handler functions)

我们合作的(What We Work With)

The signature of the handler function is the same as a raw request to GraphQL. Looking at the request body to GraphQL in the Network tab of a browser, you’ll see the 3 variables accessible to use when mocking.

处理函数的签名与对GraphQL的原始请求相同。 在浏览器的“网络”选项卡中查看对GraphQL的请求正文,您将看到在进行模拟时可以使用的3个变量。

  • query — is the raw GraphQL Query you have defined in your code that is wrapped by the operation name. Variable names are not filled in. I don’t find this to be useful, but it’s good to know it’s there.

    query —是您在代码中定义的原始GraphQL查询,由操作名称包装。 变量名没有填写。我觉得这没什么用,但是很高兴知道它在那里。

  • operationName — is what you’ve defined this query to be. It comes from the portion of a query that wraps the variables.

    operationName —是您定义此查询的对象。 它来自查询中包装变量的部分。

  • variables — the values that the GraphQL server will inject into the query to execute it.

    变量-GraphQL服务器将注入查询以执行查询的值。

实作 (Implementation)

When determining how to respond to the request, I’ve created two maps to track all requests and responses.

在确定如何响应请求时,我创建了两个映射来跟踪所有请求和响应。

// <String, Number>
let graphQLRequestMap = {};// <String, Map<Number, JSON>>
let graphQLResponseMap = {};
  • graphQLRequestMap — uses a string as a key to track the number of executions of a request. The key is the GraphQL request’s operationName and variables objects run through JSON.stringify. Although, sometimes it will just be the operationName.

    graphQLRequestMap —使用字符串作为键来跟踪请求的执行次数。 关键是GraphQL请求的operationName和通过JSON.stringify运行的variables对象。 尽管有时它只是operationName

  • graphQLResponseMap — uses the same key, but has a second map that determines the response based on the number of times that key was called.

    graphQLResponseMap —使用相同的键,但具有第二个映射,该映射根据键被调用的次数确定响应。

This is abstract, but will become clearer once you look at how they are used. The goal is to allow you to configure your mock as specifically or generically as you would like. Let’s take a look at how these are used.

这是抽象的,但是一旦您了解它们的用法,它将变得更加清晰。 目的是允许您根据需要专门或通用地配置模拟。 让我们看看如何使用它们。

const GLOBAL_LOOKUP = '*';

// <String, Number>
let gqlRequestMap = {};

// <String, Map<Number, JSON>>
let gqlResponseMap = {};

const resetGraphQLMock = () => {
gqlRequestMap = {};
gqlResponseMap = {};
};

const initializeRequestMap = (requestKey) => {
if (gqlRequestMap[requestKey] === undefined) {
gqlRequestMap[requestKey] = 0;
}
}

const responseMapHasKey = (request, count) =>
gqlResponseMap[request] !== undefined &&
gqlResponseMap[request][count] !== undefined;

const getResponse = (request, count) => {
gqlRequestMap[request] += 1;
const result = gqlResponseMap[request][count];
return getResponseStub(result);
};

const getResponseStub = (result) => {
return Promise.resolve({
json() {
return Promise.resolve(result);
},
text() {
return Promise.resolve(JSON.stringify(result));
},
ok: true
});
};

const getRequest = (requestJSON) => {
const request = JSON.stringify(requestJSON);
initializeRequestMap(request);
const counter = gqlRequestMap[request];
return { request, counter };
}

const processGraphQLResponse = (requestBody) => {
// `body` also contains `query` and we need to remove it
const { operationName, variables } = JSON.parse(requestBody);
// Return the most specific response we can find
const {
request: specificRequest,
counter: specificCounter
} = getRequest({ operationName, variables });
if (responseMapHasKey(specificRequest, specificCounter)) {
return getResponse(specificRequest, specificCounter);
}
if (responseMapHasKey(specificRequest, GLOBAL_LOOKUP)) {
return getResponse(specificRequest, GLOBAL_LOOKUP);
}
// Return a common response to all requests if it exists
const {
request: genericRequest,
counter: genericCounter
} = getRequest({ operationName });
if (responseMapHasKey(genericRequest, genericCounter)) {
return getResponse(genericRequest, genericCounter);
}
if (responseMapHasKey(genericRequest, GLOBAL_LOOKUP)) {
return getResponse(genericRequest, GLOBAL_LOOKUP);
}
console.log(
'Not found counter/operation/variables: ',
specificCounter,
operationName,
variables
);
return getResponseStub({});
};

Focusing on processGraphQLResponse, you can see that the goal is to load the specific response we can for a given GraphQL request. We start by looking for a response with the operationName and the variables that matches the specific counter of the request. If we don’t find one, we look for a global response. Then we fallback to a response that is associated with only the operationName, using the same specific counter or global lookup logic. This gives us the flexibility to respond appropriately to pagination operations or mutations and reloads.

关注processGraphQLResponse ,您可以看到目标是为给定的GraphQL请求加载特定的响应。 我们首先寻找一个带有operationName和与请求的特定计数器匹配的variables的响应。 如果找不到,我们将寻求全球的回应。 然后,我们使用相同的特定计数器或全局查找逻辑回退到仅与operationName相关联的响应。 这使我们可以灵活地对分页操作或突变和重新加载做出适当的响应。

Having the console.log is helpful while creating a test or making changes to a test after the application is modified.

在创建测试或在修改应用程序之后对测试进行更改时,拥有console.log会很有帮助。

装满模拟 (Filling the Mock)

Now that we know how both tracking maps are used, let’s look at how to fill the mock response data.

现在我们知道如何使用两种跟踪地图,让我们看看如何填充模拟响应数据。

Cypress.Commands.add(
'addGraphQLServerResponse',
(requestDetailsInput, responseBody, allRequests = false) => {
let requestDetails;
if (typeof requestDetailsInput === 'string') {
requestDetails = JSON.stringify({
operationName: requestDetailsInput
});
} else {
requestDetails = JSON.stringify(requestDetailsInput);
}
if (allRequests) {
gqlResponseMap[requestDetails] = [];
gqlResponseMap[requestDetails][GLOBAL_LOOKUP] = responseBody;
} else if (gqlResponseMap[requestDetails] === undefined) {
gqlResponseMap[requestDetails] = [];
gqlResponseMap[requestDetails][0] = responseBody;
} else {
const array = gqlResponseMap[requestDetails];
gqlResponseMap[requestDetails][array.length] = responseBody;
}
}
);

Let’s look at the method signature first:

首先让我们看一下方法签名:

  1. requestDetailsInput — is either a string or json. If it is a string it should be the GraphQL operationName and this method will convert it into an object that is consistent with how the requests are managed. If it is json, we expect it to contain the operationName and optionally, the variables. However, if you don’t provide variables, you should pass the operationName as a string to keep your code cleaner.

    requestDetailsInput —是stringjson 。 如果是string ,则应为GraphQL operationName ,此方法会将其转换为与如何管理请求一致的对象。 如果它是json ,我们希望它包含operationName和可选的variables 。 但是,如果不提供variables ,则应将operationName作为string传递,以使代码更简洁。

  2. responseBody — is a JSON object (not a string) that contains the entire graphQL response. It can be stored in a separate file to keep your code clean, but you must load it prior to calling this. We haven’t replicated cy.route‘s support for file loading.

    responseBody —是一个JSON对象(不是string ),包含整个graphQL响应。 可以将其存储在一个单独的文件中,以保持代码的清洁,但是必须在调用之前加载它。 我们尚未复制cy.route对文件加载的支持。

  3. allRequests — is a boolean that distinguishes if this configuration should be used if we don’t have anything more specific. The default is false.

    allRequests —是一个布尔值,用于区分我们是否没有更具体的内容是否应使用此配置。 默认值为false

用法 (Usage)

Here is an example of how to use the new cypress commands.

这是如何使用新的cypress命令的示例。

import getMyStudentsResp from './my_students_response';
import getMyStudentsWithStudent3Resp from './my_students_response_with_3';
import DUMMY_STUDENT_DATA from './dummy_student_data';const STUDENT_1_UUID = 'some-uuid-1';
const STUDENT_2_UUID = 'some-uuid-2';
const STUDENT_3_UUID = 'some-uuid-3';
const getStudent1Resp = { STUDENT_1_UUID, ...DUMMY_STUDENT_DATA };
const getStudent2Resp = { STUDENT_2_UUID, ...DUMMY_STUDENT_DATA };
const getStudent3Resp = { STUDENT_3_UUID, ...DUMMY_STUDENT_DATA };context('Test Student List', () => {
beforeEach(() => {
cy.mockGraphQLServer();
cy.addGraphQLServerResponse('getMyStudents', getMyStudentsResp);}); it('loads student 1 and 2 properly', () => {
cy.addGraphQLServerResponse({
operationName: 'getStudent',
variables: { studentId: STUDENT_1_UUID }
}, getStudent1Resp);cy.addGraphQLServerResponse({
operationName: 'getStudent',
variables: { studentId: STUDENT_2_UUID }
}, getStudent2Resp); // rest of the test...
}); it('adds student 3 and reload lists', () => {
cy.addGraphQLServerResponse({
operationName: 'addStudent',
variables: { studentId: STUDENT_3_UUID, ...DUMMY_STUDENT_DATA }
}, getStudent3Resp); cy.addGraphQLServerResponse('getMyStudents', getMyStudentsWithStudent3Resp);// rest of test...
});

});

And there you have it. One way to mock your GraphQL in Cypress. I hope this helps!

那里有。 在赛普拉斯中模拟GraphQL的一种方法。 我希望这有帮助!

翻译自: https://medium.com/life-at-paperless/cypress-graphql-response-mocking-7d49517f7754

赛普拉斯天线

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值