The other day, checking my code, I found a graphQL query that was repeating in many places. So I decided to put that query into a custom hook. That was the easy part, the hard part was to know how to test it. This is how I did it:
For this tutorial, we will be using this Public GraphQL API for information about countries to fetch the country’s names and codes. This is the query:
query {
countries {
name
code
}
}
Now it’s time to create our custom hook, which it’s pretty straightforward.
The custom hook has two duties, the first one is to fetch the list of countries and the second one is to concatenate the country’s name and code.
/**
* Custom hook to fecth a list of countries
*
* @export
* @param {Object} [options={}] Apollo Query Options
* @returns {Object} Object with the the countries, loading, and error variables.
*/
export default function useCountries(queryOptions = {}) {
const { loading, error, data } = useQuery(COUNTRIES, queryOptions);
const countries = useMemo(() => {
if (data) {
return data.countries.map(country => `${country.name} - ${country.code}`);
}
}, [data]);
return { loading, error, countries };
}
Let’s see it in action
import React from "react";
import useCountries from "./hooks/useCountries";
import "./styles.css";
export default function App() {
const { loading, error, countries } = useCountries();
function renderCountryList(country, index) {
return (
<div className="list-item" key={index}>
{country}
</div>
);
}
if (loading) {
return <h2>Loading countries</h2>;
}
if (error) {
return <h2>Uppps! There was an error</h2>;
}
return (
<div className="App">
<h1>List of Countries</h1>
{countries.map(renderCountryList)}
</div>
);
}
How to test it
Now comes the fun part, how to test that hook. We will be using React Testing Library, @apollo/react-testing to mock our Apollo Provider, and react-hooks-testing-library
Let's start by creating our test cases and our mocked responses. We will be testing when it gets the list of countries successfully and when there is an error.
import React from "react";
import useCountries, { COUNTRIES } from "./useCountries";
describe("useCountries custom hook", () => {
// :: DATA ::
const mexico = {
name: "Mexico",
code: "MX"
};
const argentina = {
name: "Argentina",
code: "AR"
};
const portugal = {
name: "Portugal",
code: "PT"
};
// :: MOCKS ::
const countriesQueryMock = {
request: {
query: COUNTRIES
},
result: {
data: {
countries: [argentina, mexico, portugal]
}
}
};
const countriesQueryErrorMock = {
request: {
query: COUNTRIES
},
error: new Error("Ohh Ohh!")
};
it("should return an array of countries", async () => {});
it("should return error when request fails", async () => {});
});
First test case
The first test case checks that our hook returns an array of countries.
If you read the documentation of react-hooks-testing-library
, you know that we have to wrap our hook in renderHook:
const { result, waitForNextUpdate } = renderHook(() => useCountries());
But because we are using useQuery from Apollo inside our hook, we need to use MockedProvider to wrap renderHook and Mock the responses. We can use the wrapper option for renderHook to do that.
// Apollo Mocked Provider Wrapper
const wrapper = ({ children }) => (
<MockedProvider>
{children}
</MockedProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useCountries(), {
wrapper
});
Because we will be using that code in both of our test cases, we can move it into a function
function getHookWrapper(mocks = []) {
const wrapper = ({ children }) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useCountries(), {
wrapper
});
// Test the initial state of the request
expect(result.current.loading).toBeTruthy();
expect(result.current.error).toBeUndefined();
expect(result.current.countries).toBeUndefined();
return { result, waitForNextUpdate };
}
Now we test the first case.
it("should return an array of countries", async () => {
const { result, waitForNextUpdate } = getHookWrapper([countriesQueryMock]);
// Wait for the results
await waitForNextUpdate();
// We access the hook result using result.current
expect(result.current.loading).toBeFalsy();
expect(result.current.error).toBeUndefined();
expect(result.current.countries).toEqual([
`${argentina.name} - ${argentina.code}`,
`${mexico.name} - ${mexico.code}`,
`${portugal.name} - ${portugal.code}`
]);
});
Second test case
The second test case it's pretty similar, but now we test when there's an error.
it("should return error when request fails", async () => {
// Similar to the first case, but now we use countriesQueryErrorMock
const { result, waitForNextUpdate } = getHookWrapper([
countriesQueryErrorMock
]);
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.error).toBeTruthy();
expect(result.current.countries).toBeUndefined();
});
As you can see, it's not that hard once you know how to do it. Here is the code in case you need it.
Thanks for reading.
Top comments (4)
Thanks for this! From this setup, how would you go about changing the input to the hook and checking the hook's new return values? e.g if you wanted to change queryOptions, but the hook cares about old queries too?
I created this hook because I was using the query all over the place and in my case, I know that both the query and the input to the custom hook won't change. The only thing that could change is the
queryOptions
, maybe I want to change thefetchPolicy
option or add a callback when the request is completed using theonCompleted
option. But as I said, that is the only thing that could change.Greate job Hugo!
I wondering how to do this same example using the
useLazyQuery
Hi Giolvani! I changed to the to use
useLazyQuery
you can check it here