当我学习一项新技术时,我首先想到的是 "好吧,我如何使用这个?"但不久之后,问题就变成了 "我如何测试这个?"
最近在我的React项目中使用Apollo GraphQL就是这种情况。在这篇文章中,我们将学习如何测试一个使用GraphQL查询的组件。
TLDR:有两个关键来正确处理这个问题
TLDR是,有两个关键来获得这个权利。
- 使用
@apollo/client/testing库中的MockedProvider,将我们的组件包裹在测试中。 - 提供与你要模拟的查询完全匹配的模拟。
一个组件的例子
让我们创建一个非常简单的组件。它将使用现成的github GraphQL API。我已经假设你有一个安装了Apollo GraphQL的React项目。
我们的查询基本上是在github上搜索与查询词 "javascript "相匹配的前10个软件库。下面这个组件就是这样做的
import { useQuery, gql } from '@apollo/client';
export const TOP_PROJECTS = gql`
query SearchTopProjects($queryString: String!) {
search(query: $queryString, type: REPOSITORY, first: 10) {
edges {
node {
... on Repository {
name
description
stargazers {
totalCount
}
}
}
}
}
}
`;
export function TopProjects() {
const { loading, error, data } = useQuery(TOP_PROJECTS, {
variables: {
queryString: 'javascript',
},
});
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Oh no!</p>;
}
return (
<ul>
{data.search.edges.map(({ node }) => (
<li key={node.name}>
{node.description} | {node.stargazers.totalCount} Stars
</li>
))}
</ul>
);
}
那么......我们如何测试它呢?
编写一个测试
让我们创建一个名为TopProjects.test.jsx 的测试文件。在这个文件中,我们将使用React测试库来渲染我们的组件。重要的是,我们要用从@apollo/client/testing 输出的MockedProvider 来包装我们的组件。MockedProvider 将一个mocks 数组作为属性,代表我们要模拟的所有graphql调用。
下面是我们的测试可能开始的方式。
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects } from './TopProjects';
const mocks = [];
test('renders top project list', () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
在这里,我们只是期望我们渲染的HTML能与快照相匹配。当然,这是不可能的,因为我们还没有模拟出我们的graphql调用!
GraphQL调用
这里要做的最重要的事情是确保你的GraphQL查询和提供的变量与你的组件所使用的变量完全相同。记住,GraphQL的伟大之处在于它的灵活性,但我们必须确保在测试时小心翼翼地重复查询。
你可能已经注意到,我们已经从我们的TopProjects 文件中导出了TOP_PROJECTS graphql 查询--这不是错误!我们现在可以使用该查询来帮助复制我们的模拟请求的形状。
import { TopProjects, TOP_PROJECTS } from './TopProjects';
const mocks = [
{
request: {
query: TOP_PROJECTS,
variables: {
queryString: 'javascript',
},
},
result: {
data: {},
},
},
];
请注意,我的查询是由组件导出的GraphQL查询的例子,而且提供的variables 与组件的要求完全一致。这一点很关键,因为偏差意味着MockProvider 会认为这是一个完全不同的查询。
所以这个模拟还没有完全完成。我们需要指定我们将得到的数据作为回报。指定数据的一个有用的方法是在浏览器中查看你的网络标签,并复制服务器的响应。它是非常冗长的,所以我只给你一个预览。
{
"data": {
"search": {
"edges": [
{
"node": {
"name": "javascript",
"description": "JavaScript Style Guide",
"stargazers": {
"totalCount": 104737,
"__typename": "StargazerConnection"
},
"__typename": "Repository"
},
"__typename": "SearchResultItemEdge"
}
/* Plus 9 more nodes here */
],
"__typename": "SearchResultItemConnection"
}
}
}
因此,为了模拟这种响应而不必写出一大堆行,我们将创建一个方便的createRepo 函数,它将为我们创建每个节点。最后,我们的测试文件看起来是这样的。
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects, TOP_PROJECTS } from './TopProjects';
function createRepo(name, description, stars) {
return {
node: {
name,
description,
stargazers: {
totalCount: stars,
__typename: 'StargazerConnection',
},
__typename: 'Repository',
},
__typename: 'SearchResultItemEdge',
};
}
const mocks = [
{
request: {
query: TOP_PROJECTS,
variables: {
queryString: 'javascript',
},
},
result: {
data: {
search: {
edges: [
createRepo('js-stuff', 'Some JS stuff', 1000000),
createRepo('jsdoc', 'Make docs for JS', '900000'),
createRepo('anotherexample', 'Some othre description', '20000'),
],
__typename: 'SearchResultItemConnection',
},
},
},
},
];
test('renders learn react link', () => {
const { container, getByText } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
因此,让我们用yarn test (或者npm,如果你使用的话)来运行测试。
yarn test
看起来像是在__snapshots__ 文件夹中创建了一个快照。让我们检查一下吧
exports[`renders top project list 1`] = `
<div>
<p>
Loading...
</p>
</div>
`;
哦,不!我们看到了我们的加载指标。好吧,这实际上是有意义的,因为这是我们的用户在我们的查询解决之前最初加载应用程序时看到的东西。
处理异步行为
为了处理这个异步行为,我们做了几件事。
- 改变我们的测试函数为
async,这样我们就可以使用await - 等待调用栈清除
以下是我们如何做的。
import { render, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects, TOP_PROJECTS } from './TopProjects';
function createRepo(name, description, stars) {
// Removed code for clarity
}
const mocks = [
// Removed code for clarity
];
test('renders top project list', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
await waitFor(() => new Promise((res) => setTimeout(res, 0)));
expect(container).toMatchSnapshot();
});
waitFor 行背后的想法是,setTimeout 回调,即使有0秒的超时,也会将代码的执行放在事件队列中,从而在调用栈清除之前不被执行。在我们的例子中,这意味着在我们的模拟提供者返回模拟查询值并渲染它之后,Promise才会解析。
让我们再次运行我们的测试并查看我们的快照文件。
xports[`renders top project list 1`] = `
<div>
<ul>
<li>
Some JS stuff
|
1000000
Stars
</li>
<li>
Make docs for JS
|
900000
Stars
</li>
<li>
Some othre description
|
20000
Stars
</li>
</ul>
</div>
`;
啊哈!这正是我们想要看到的。我们现在可以用react测试库和jest做任何其他我们想做的断言,因为我们知道我们完全控制了被模拟的数据。
祝您好运!