Skip to content

Commit 6edaf2d

Browse files
committed
Add apollo react testing example
1 parent 3c63a00 commit 6edaf2d

File tree

9 files changed

+129
-50
lines changed

9 files changed

+129
-50
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"graphql-tag": "^2.10.1",
1919
"graphql-tools": "^4.0.5",
2020
"graphql.macro": "^1.4.2",
21+
"ramda": "^0.26.1",
2122
"react": "^16.9.0",
2223
"react-apollo": "^3.0.0",
2324
"react-dom": "^16.9.0",
@@ -43,5 +44,8 @@
4344
"last 1 firefox version",
4445
"last 1 safari version"
4546
]
47+
},
48+
"devDependencies": {
49+
"@apollo/react-testing": "^3.0.0"
4650
}
4751
}

src/App.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import React from "react";
22
import "./App.css";
3-
import Repository from "./Repository";
4-
import ApolloMockingProvider from "./mocking/ApolloMockingProvider";
3+
import Repository, { gqlQuery } from "./Repository";
4+
import ApolloMockingProvider, { getClient } from "./mocking/ApolloMockingProvider";
55

66
function App() {
7+
// Demo random data generation (maybe disable defaultMocks?)
8+
// getClient({})
9+
// .query({ query: gqlQuery(1) })
10+
// .then(res => console.log("Random data", res.data));
11+
712
return (
813
<div className="App">
914
<ApolloMockingProvider customResolvers={{}}>

src/Repository.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import gql from "graphql-tag";
33
import { Query } from "react-apollo";
44

5-
const gqlQuery = ghId => {
5+
export const gqlQuery = ghId => {
66
return gql`
77
{
88
repositoryByGhId(ghId: ${ghId}) {

src/Repository.test.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import React from "react";
2-
import Repository from "./Repository";
3-
import { render } from "./mocking/ApolloMockingProvider";
2+
import Repository, { gqlQuery } from "./Repository";
3+
import { getClient, render } from "./mocking/ApolloMockingProvider";
44

55
const zenhubRepo = {
6-
Repository: () => ({
6+
Repository: {
77
name: "ZenHubHQ"
8-
})
8+
}
99
};
1010

1111
const threeIssues = {
12-
Repository: () => ({
12+
Repository: {
1313
issues: [
1414
{ number: 1, title: "Issue 1" },
1515
{ number: 2, title: "Issue 2" },
1616
{ number: 3, title: "Issue 3" }
1717
]
18-
})
18+
}
1919
};
2020

2121
describe("<Repository />", () => {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from "react";
2+
import * as R from "ramda";
3+
import Repository, { gqlQuery } from "./Repository";
4+
import { MockedProvider } from "@apollo/react-testing";
5+
import { render } from "@testing-library/react";
6+
import { getClient } from "./mocking/ApolloMockingProvider";
7+
8+
describe("<Repository alternative mocking/>", () => {
9+
test("renders repository with official apollo mocked provider", async () => {
10+
const mocks = [
11+
{
12+
request: {
13+
query: gqlQuery(1)
14+
},
15+
result: {
16+
data: {
17+
repositoryByGhId: {
18+
ghId: 1,
19+
name: "ZenHubHQ",
20+
description: "hello world",
21+
isPrivate: true,
22+
updatedAt: new Date(),
23+
24+
issues: []
25+
}
26+
}
27+
}
28+
}
29+
];
30+
31+
const { findByText } = render(
32+
<MockedProvider mocks={mocks} addTypename={false}>
33+
<Repository ghId={1} />
34+
</MockedProvider>
35+
);
36+
37+
await findByText("ZenHubHQ");
38+
});
39+
40+
test("renders repository with official apollo mocked provider but automatically generated data", async () => {
41+
const preRes = await getClient().query({ query: gqlQuery(1) });
42+
43+
const result = R.assocPath(["data", "repositoryByGhId", "name"], "ZenHubHQ", preRes);
44+
45+
const mocks = [
46+
{
47+
request: {
48+
query: gqlQuery(1)
49+
},
50+
result
51+
}
52+
];
53+
54+
const { findByText } = render(
55+
<MockedProvider mocks={mocks} addTypename={false}>
56+
<Repository ghId={1} />
57+
</MockedProvider>
58+
);
59+
60+
await findByText("ZenHubHQ");
61+
});
62+
});

src/mocking/ApolloMockingProvider.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import mergeResolvers from "./mergeResolvers";
1515

1616
const ApplicationSchema = loader("./schema.graphql");
1717

18-
// For queries with no variables
1918
const schema = makeExecutableSchema({ typeDefs: ApplicationSchema });
2019

21-
export const getClient = customResolvers => {
20+
export const getClient = (customResolvers = {}) => {
2221
const mocks = mergeResolvers([defaultMocks, customResolvers]);
2322

2423
addMockFunctionsToSchema({ schema, mocks });

src/mocking/defaultMocks.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ const jan2019 = new Date(1546510000000);
2424

2525
const resolvers = {
2626
ISO8601DateTime: () => jan2019,
27-
Repository: () => ({
28-
ghId: getNextId,
29-
name: formatRepoName(faker.commerce.productName()),
30-
description: faker.company.catchPhrase()
31-
}),
32-
Issue: () => ({
27+
Repository: {
28+
ghId: () => getNextId(),
29+
name: () => formatRepoName(faker.commerce.productName()),
30+
description: () => faker.company.catchPhrase()
31+
},
32+
Issue: {
3333
number: getNextId,
3434
title: () => faker.hacker.phrase(),
35-
state: () => (Math.random() > 0.5 ? "open" : "closed")
36-
})
35+
state: () => "open"
36+
}
3737
};
3838

3939
export default resolvers;

src/mocking/mergeResolvers.js

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copy pasted (without typescript) from https://raw.githubusercontent.com/ardatan/graphql-toolkit/master/src/epoxy/resolvers-mergers/merge-resolvers.ts, the package doesn't work with webpack (missing fs) */
1+
/* Simplified version of https://raw.githubusercontent.com/ardatan/graphql-toolkit/master/src/epoxy/resolvers-mergers/merge-resolvers.ts, the package doesn't work with webpack (missing fs) */
22

33
import * as deepMerge from "deepmerge";
44
import { GraphQLScalarType } from "graphql";
@@ -23,42 +23,35 @@ const isMergeableObject = target => {
2323
return true;
2424
};
2525

26-
function mergeResolvers(resolversDefinitions, options) {
26+
// IMPORTANT: When converting object to function, always return a copy of the resolver instead of the resolver:
27+
// mergeObjects mutates the resolver which would result in strange behaviours
28+
29+
// (addMockFunctionsToSchema -> mergeMocks -> mergeObjects)
30+
function convertObjectTypesToFunctions(resolvers) {
31+
return Object.keys(resolvers).reduce((acc, key) => {
32+
if (typeof resolvers[key] === "function") {
33+
acc[key] = resolvers[key];
34+
return acc;
35+
} else if (typeof resolvers[key] === "object") {
36+
acc[key] = () => ({ ...resolvers[key] }); // See top function comment
37+
return acc;
38+
} else {
39+
throw new Error("Mock entry must be object or function");
40+
}
41+
}, {});
42+
}
43+
44+
// TODO PR Explain the magic
45+
function mergeResolvers(resolversDefinitions) {
2746
if (!resolversDefinitions || resolversDefinitions.length === 0) {
2847
return {};
2948
}
3049
if (resolversDefinitions.length === 1) {
3150
return resolversDefinitions[0];
3251
}
33-
const resolversFactories = [];
34-
const resolvers = [];
35-
for (const resolversDefinition of resolversDefinitions) {
36-
if (typeof resolversDefinition === "function") {
37-
resolversFactories.push(resolversDefinition);
38-
} else if (typeof resolversDefinition === "object") {
39-
resolvers.push(resolversDefinition);
40-
}
41-
}
42-
let result = {};
43-
if (resolversFactories.length) {
44-
result = (...args) => {
45-
const resultsOfFactories = resolversFactories.map(factory => factory(...args));
46-
return deepMerge.all([...resolvers, ...resultsOfFactories], { isMergeableObject });
47-
};
48-
} else {
49-
result = deepMerge.all(resolvers, { isMergeableObject });
50-
}
51-
if (options && options.exclusions) {
52-
for (const exclusion of options.exclusions) {
53-
const [typeName, fieldName] = exclusion.split(".");
54-
if (!fieldName || fieldName === "*") {
55-
delete result[typeName];
56-
} else if (result[typeName]) {
57-
delete result[typeName][fieldName];
58-
}
59-
}
60-
}
61-
return result;
52+
const resolvers = deepMerge.all(resolversDefinitions, { isMergeableObject });
53+
54+
return convertObjectTypesToFunctions(resolvers);
6255
}
6356

6457
export default mergeResolvers;

0 commit comments

Comments
 (0)