赛普拉斯天线
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:
此代码段执行以下操作:
- Resets the GraphQL Mock requests tracker and response map. 重置GraphQL Mock请求跟踪器和响应图。
- Before the window loads execute the following. 在加载窗口之前,请执行以下操作。
Store the existing
fetch
for default behavior if not a GraphQL request.如果不是GraphQL请求,则将现有的
fetch
存储为默认行为。- 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,并调用我们将创建的处理程序来处理任务。 否则,请使用原始获取。
Then overwrite the window’s fetch function with the newly created one and alias to
graphqlStub
. This alias is different than the ones used withcy.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
andvariables
objects run throughJSON.stringify
. Although, sometimes it will just be theoperationName
.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:
首先让我们看一下方法签名:
requestDetailsInput
— is either astring
orjson
. If it is astring
it should be the GraphQLoperationName
and this method will convert it into an object that is consistent with how the requests are managed. If it isjson
, we expect it to contain theoperationName
and optionally, thevariables
. However, if you don’t providevariables
, you should pass theoperationName
as astring
to keep your code cleaner.requestDetailsInput
—是string
或json
。 如果是string
,则应为GraphQLoperationName
,此方法会将其转换为与如何管理请求一致的对象。 如果它是json
,我们希望它包含operationName
和可选的variables
。 但是,如果不提供variables
,则应将operationName
作为string
传递,以使代码更简洁。responseBody
— is a JSON object (not astring
) 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 replicatedcy.route
‘s support for file loading.responseBody
—是一个JSON对象(不是string
),包含整个graphQL响应。 可以将其存储在一个单独的文件中,以保持代码的清洁,但是必须在调用之前加载它。 我们尚未复制cy.route
对文件加载的支持。allRequests
— is a boolean that distinguishes if this configuration should be used if we don’t have anything more specific. The default isfalse
.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
赛普拉斯天线