Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documented API for MockedProvider #3309

Merged
merged 9 commits into from
Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ sidebar_categories:
- advanced/subscriptions
- advanced/network-layer
- advanced/caching
- advanced/testing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this :)

- advanced/fragments
Recipes:
- recipes/authentication
Expand Down
308 changes: 308 additions & 0 deletions docs/source/advanced/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file can go away in favor of other docs, too

title: Testing
description: Testing react-apollo
---

React-apollo relies on [context](https://reactjs.org/docs/context.html) in order to pass the apollo-client instance through the react component tree. In addition, react-apollo makes network requests in order to fetch data. This behavior affects how you write tests for components that use react-apollo.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistic thing: the docs refer to react-apollo as "React Apollo" and apollo-client as Apollo Client for the most part. This document should follow that.

"React" should be capitalized (i.e. not "react").


This guide will explain step-by-step how you can test your react-apollo code.

Consider the example below:

```js
import gql from "graphql-tag";
import { Query } from "react-apollo";

const GET_DOG = gql`
query getDog($name: String) {
dog(name: $name) {
id
name
breed
}
}
`;

export const Dog = ({ name }) => (
<Query query={GET_DOG} variables={{ name }}>
{({ loading, error, data }) => {
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;

return (
<div>
{data.dog.name} has breed {data.dog.breed}
</div>
);
}}
</Query>
);
```

If we were to try and write a test for it then you will get an error that the `client` is missing in the context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What "it" refers to is a bit unclear here; maybe "If we were to write a test for this component, we'd get an error that the client object is missing in the context."


```js
//broken because of a missing apollo-client in the context
test("it shows a dog", () => {
render(<Dog name="Buck" />);
expect(dogsToBePresent);
});
```

In order to fix this we could wrap the component in an `<ApolloProvider />` and pass an instance of apollo-client to the `client` prop. However, this will cause our tests to run against an actual back-end which makes the tests very unpredictable for the following reasons:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistic: replace "back-end" with "backend."


* The server could be down
* No network connection
* The results are not guaranteed to be the same for every query.

```js
// Not predictable
test("it shows a dog", () => {
render(
<ApolloProvider client={client}>
<Dog name="Buck" />
</ApolloProvider>
);
expect(dogsToBePresent);
});
```

To test the component in isolation we need to mock out all the calls to the backend. This will make our tests predictable. React-apollo provides the `<MockedProvider />` component in order to do just that! `<MockedProvider />` allows you to specify the exact results that should be returned for a certain query using the `mocks` prop.

Let's see how we can write the example using this component:

First we need to export the `GET_DOG` query that we are sending to the graphQL API:

```js
// Export the query
export const GET_DOG = gql`
query getDog($name: String) {
dog(name: $name) {
id
name
breed
}
}
`;

export const Dog = (...)
```

Next, we go to our test file and define the mocked result that should be returned for the query. `mocks` is an array that takes an object with specific `request` and the associated `result`.

```js
import { GET_DOG, Dog } from "./dog";

const mocks = [
{
request: {
query: GET_DOG,
variables: {
name: "Buck"
}
},
result: {
data: {
dog: {
id: "1",
name: "Buck",
breed: "bulldog"
}
}
}
}
];

test("it shows a dog", () => {
render(
<MockedProvider mocks={mocks}>
<Dog name="Buck" />
</MockedProvider>
);

//Fails because react-apollo will initially show a loading state
expect(dogsToBePresent);
});
```

Despite having mocked the request the above test will still fail. Due to the asynchronous nature of react-apollo the loading state will be rendered instead of the dog name.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is a bit unclear to me. What is meant by "due to the asynchronous nature of React Apollo"?


In order to overcome this issue we advise you separate the react-apollo components from presentational components and test these independently.

Let's see how we can refactor the code to improve testability.

```js
export const GET_DOG = gql`
query getDog($name: String) {
dog(name: $name) {
id
name
breed
}
}
`;

// The react-apollo component
export const DogQuery = ({ children, name }) => (
<Query query={GET_DOG} variables={{name}}>
{({ loading, error, data }) =>
children({
loading,
error,
dog: data && data.dog
})
}
</Query>
);

// The presentational component
export const DogInterface = props => {
const { loading, error, dog } = props;
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;

return <div>{dog.name} has breed {dog.breed}</div>;
};

// Connecting the components together.
export const Dog = props => (
<DogQuery name={props.name}>{result => <DogInterface {...result} />}</DogInterface>
);
```

Let's go back to our test file and see how we can test the `<DogQuery />` component.

```js
import { GET_DOG, DogQuery } from "./dog";

const expectedRequest = {
query: GET_DOG,
name: "Buck"
}

const mocks = [
{
request: expectedRequest,
result: {
data: {
dog: {
id: "1",
name: "Buck",
breed: "bulldog"
}
}
}
}
];

describe("dog with query", () => {
it("renders a loading and then dog", done => {
let renderCount = 0;

render(
<MockedProvider mocks={mocks}>
<DogQuery name="Buck">
{result => {
if (renderCount === 0) {
// The first render has the loading state
expect(result.loading).toBe(true);
expect(result.dog).toBe(undefined);
}
else if (renderCount === 1) {
// The second render has the data.
expect(result.loading).toBe(false);
expect(result.dog).toEqual({
id: "1",
name: "Buck",
breed: "bulldog"
});
done()
}

renderCount++;

return null;
}}
</DogWithQuery>
</MockedProvider>
);
});

it("renders an error", done => {
//<MockedProvider /> can also be used to test error states.
const mocksWithError = [
{
request: expectedRequest,
error: new Error('Something went wrong'),
},
];

let renderCount = 0;

render(
<MockedProvider mocks={mocksWithError}>
<DogQuery name="Buck">
{result => {
// Second render has the error state
if (renderCount === 1) {
expect(result).toEqual({
error: new Error('Network error: Something went wrong'),
loading: false,
});
done();
}

renderCount++;
return null;
}}
</HeroQuery>
</MockedProvider>
);
})
});
```

Next, let's test the `<DogInterface />` component:

```js
describe("<DogInterface />", () => {
it("renders the dog", () => {
const dog = {
id: "1"
name: "Buck",
breed: "bulldog"
};

render(<DogInterface dog={dog} />);
expect(dogToBeShown);
});

it("renders a loading state", () => {
render(<DogInterface loading={true} />);
expect(loadingToBeShown);
});

it("renders an error state", () => {
render(<DogInterface error={new Error("Something went wrong")} />);
expect(errorToBeShown);
});
});
```

Finally, we need to test that the `<Dog />` component has correctly connected the `<DogQuery />` and `<DogInterface />` together.

```js
it("renders <Dog />", () => {
render(
<MockedProvider mocks={mocks}>
<Dog name="Buck" />
</MockedProvider>
);

expect(loadingToBeShown);
});
```

We have now successfully tested the code!

In case you want some more information, we have created an [example repo](https://github.com/apollographql/react-apollo/tree/master/examples/components) that illustrates how you can use the techniques outlined in this guide to test your components.