|
| 1 | +--- |
| 2 | +Title: Testing components with GraphQL |
| 3 | +--- |
| 4 | + |
| 5 | +If you try to run unit tests on components that use GraphQL queries, you will |
| 6 | +discover that you have no data. Jest can't run your queries, so if you are |
| 7 | +testing components that rely on GraphQL data, you will need to provide the data |
| 8 | +yourself. This is a good thing, as otherwise your tests could break if your data |
| 9 | +changes, and in the case of remote data sources it would need network access to |
| 10 | +run tests. |
| 11 | + |
| 12 | +In general it is best practice to test the smallest components possible, so the |
| 13 | +simplest thing to do is to test the individual page components with mock data, |
| 14 | +rather than trying to test a full page. However, if you do want to test the full |
| 15 | +page you'll need to provide the equivalent data to the component. Luckily |
| 16 | +there's a simple way to get the data you need. |
| 17 | + |
| 18 | +First you should make sure you have read |
| 19 | +[the unit testing guide](/docs/unit-testing/) and set up your project as |
| 20 | +described. This guide is based on the same blog starter project. You will be |
| 21 | +writing a simple snapshot test for the index page. |
| 22 | + |
| 23 | +As Jest doesn't run or compile away your GraphQL queries you need to mock the |
| 24 | +`graphql` function to stop it throwing an error. If you set your project up with |
| 25 | +a mock for `gatsby` as described in the unit testing guide then this is already |
| 26 | +done. |
| 27 | + |
| 28 | +## Testing page queries |
| 29 | + |
| 30 | +As this is testing a page component you will need to put your tests in another |
| 31 | +folder so that Gatsby doesn't try to turn the tests into pages. |
| 32 | + |
| 33 | +```js |
| 34 | +// src/__tests__/index.js |
| 35 | + |
| 36 | +import React from "react" |
| 37 | +import renderer from "react-test-renderer" |
| 38 | +import BlogIndex from "../pages/index" |
| 39 | + |
| 40 | +describe("BlogIndex", () => |
| 41 | + it("renders correctly", () => { |
| 42 | + const tree = renderer.create(<BlogIndex />).toJSON() |
| 43 | + expect(tree).toMatchSnapshot() |
| 44 | + })) |
| 45 | +``` |
| 46 | + |
| 47 | +If you run this test you will get an error, as the component is expecting a |
| 48 | +location object. You can fix this by passing one in: |
| 49 | + |
| 50 | +```js |
| 51 | +// src/__tests__/index.js |
| 52 | + |
| 53 | +import React from "react" |
| 54 | +import renderer from "react-test-renderer" |
| 55 | +import BlogIndex from "../pages/index" |
| 56 | + |
| 57 | +describe("BlogIndex", () => |
| 58 | + it("renders correctly", () => { |
| 59 | + const location = { |
| 60 | + pathname: "", |
| 61 | + } |
| 62 | + |
| 63 | + const tree = renderer.create(<BlogIndex location={location} />).toJSON() |
| 64 | + expect(tree).toMatchSnapshot() |
| 65 | + })) |
| 66 | +``` |
| 67 | + |
| 68 | +This should fix the `location` error, but now you will have an error because |
| 69 | +there is no GraphQL data being passed to the component. We can pass this in too, |
| 70 | +but the structure is a little more complicated. Luckily there's an easy way to |
| 71 | +get some suitable data. Run `gatsby develop` and go to |
| 72 | +http://localhost:8000/___graphql to load the GraphiQL IDE. You can now get the |
| 73 | +right data using the same query that you used on the page. If it is a simple |
| 74 | +query with no fragments you can copy it directly. That is the case here, run |
| 75 | +this query copied from the index page: |
| 76 | + |
| 77 | +```graphql |
| 78 | +query IndexQuery { |
| 79 | + site { |
| 80 | + siteMetadata { |
| 81 | + title |
| 82 | + } |
| 83 | + } |
| 84 | + allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) { |
| 85 | + edges { |
| 86 | + node { |
| 87 | + excerpt |
| 88 | + fields { |
| 89 | + slug |
| 90 | + } |
| 91 | + frontmatter { |
| 92 | + date(formatString: "DD MMMM, YYYY") |
| 93 | + title |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +The output panel should now give you a nice JSON object with the query result. |
| 102 | +Here it is, trimmed to one node for brevity: |
| 103 | + |
| 104 | +```json |
| 105 | +{ |
| 106 | + "data": { |
| 107 | + "site": { |
| 108 | + "siteMetadata": { |
| 109 | + "title": "Gatsby Starter Blog" |
| 110 | + } |
| 111 | + }, |
| 112 | + "allMarkdownRemark": { |
| 113 | + "edges": [ |
| 114 | + { |
| 115 | + "node": { |
| 116 | + "excerpt": |
| 117 | + "Far far away, behind the word mountains, far from the countries Vokalia and\nConsonantia, there live the blind texts. Separated they live in…", |
| 118 | + "fields": { |
| 119 | + "slug": "/hi-folks/" |
| 120 | + }, |
| 121 | + "frontmatter": { |
| 122 | + "date": "28 May, 2015", |
| 123 | + "title": "New Beginnings" |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + ] |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +GraphiQL doesn't know about any fragments defined by Gatsby, so if your query |
| 134 | +uses them then you'll need to replace those with the content of the fragment. If |
| 135 | +you're using `gatsby-transformer-sharp` you'll find the fragments in |
| 136 | +[gatsby-transformer-sharp/src/fragments.js](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-transformer-sharp/src/fragments.js). |
| 137 | +So, for example if your query includes: |
| 138 | + |
| 139 | +```graphql |
| 140 | + image { |
| 141 | + childImageSharp { |
| 142 | + fluid(maxWidth: 1024) { |
| 143 | + ...GatsbyImageSharpFluid |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | +``` |
| 148 | + |
| 149 | +...it becomes: |
| 150 | + |
| 151 | +```graphql |
| 152 | + image { |
| 153 | + childImageSharp { |
| 154 | + fluid(maxWidth: 1024) { |
| 155 | + base64 |
| 156 | + aspectRatio |
| 157 | + src |
| 158 | + srcSet |
| 159 | + sizes |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | +``` |
| 164 | + |
| 165 | +When you have the result, copy the `data` value from the output panel. Good |
| 166 | +practice is to store your fixtures in a separate file, but for simplicity here |
| 167 | +you will be defining it directly inside your test file: |
| 168 | + |
| 169 | +```js |
| 170 | +// src/__tests__/index.js |
| 171 | + |
| 172 | +import React from "react" |
| 173 | +import renderer from "react-test-renderer" |
| 174 | +import BlogIndex from "../pages/index" |
| 175 | + |
| 176 | +describe("BlogIndex", () => |
| 177 | + it("renders correctly", () => { |
| 178 | + const location = { |
| 179 | + pathname: "", |
| 180 | + } |
| 181 | + |
| 182 | + const data = { |
| 183 | + site: { |
| 184 | + siteMetadata: { |
| 185 | + title: "Gatsby Starter Blog", |
| 186 | + }, |
| 187 | + }, |
| 188 | + allMarkdownRemark: { |
| 189 | + edges: [ |
| 190 | + { |
| 191 | + node: { |
| 192 | + excerpt: |
| 193 | + "Far far away, behind the word mountains, far from the countries Vokalia and\nConsonantia, there live the blind texts. Separated they live in…", |
| 194 | + fields: { |
| 195 | + slug: "/hi-folks/", |
| 196 | + }, |
| 197 | + frontmatter: { |
| 198 | + date: "28 May, 2015", |
| 199 | + title: "New Beginnings", |
| 200 | + }, |
| 201 | + }, |
| 202 | + }, |
| 203 | + ], |
| 204 | + }, |
| 205 | + } |
| 206 | + |
| 207 | + const tree = renderer |
| 208 | + .create(<BlogIndex location={location} data={data} />) |
| 209 | + .toJSON() |
| 210 | + expect(tree).toMatchSnapshot() |
| 211 | + })) |
| 212 | +``` |
| 213 | + |
| 214 | +Run the tests and they should now pass. Take a look in `__snapshots__` to see |
| 215 | +the output. |
| 216 | + |
| 217 | +## Testing StaticQuery |
| 218 | + |
| 219 | +The method above works for page queries, as you can pass the data in directly to |
| 220 | +the component. This doesn't work for components that use `StaticQuery` though, |
| 221 | +as that uses `context` rather than `props` so we need to take a slightly |
| 222 | +different approach to testing these. The blog starter project doesn't include |
| 223 | +`StaticQuery`, so the example here is from |
| 224 | +[the StaticQuery docs](/docs/static-query/). |
| 225 | + |
| 226 | +Using `StaticQuery` allows you to make queries in any component, not just pages. |
| 227 | +This gives a lot of flexibility, and avoid having to pass the props down to |
| 228 | +deeply-nested components. The pattern for enabling type checking described in |
| 229 | +the docs is a good starting point for making these components testable, as it |
| 230 | +separates the query from the definition of the component itself. However that |
| 231 | +example doesn't export the inner, pure component, which is what you'll need to |
| 232 | +test. |
| 233 | + |
| 234 | +Here is the example of a header component that queries the page data itself, |
| 235 | +rather than needing it to be passed from the layout: |
| 236 | + |
| 237 | +```js |
| 238 | +// src/components/Header.js |
| 239 | +import React from "react" |
| 240 | +import { StaticQuery } from "gatsby" |
| 241 | + |
| 242 | +const Header = ({ data }) => ( |
| 243 | + <header> |
| 244 | + <h1>{data.site.siteMetadata.title}</h1> |
| 245 | + </header> |
| 246 | +) |
| 247 | + |
| 248 | +export default props => ( |
| 249 | + <StaticQuery |
| 250 | + query={graphql` |
| 251 | + query { |
| 252 | + site { |
| 253 | + siteMetadata { |
| 254 | + title |
| 255 | + } |
| 256 | + } |
| 257 | + } |
| 258 | + `} |
| 259 | + render={data => <Header data={data} {...props} />} |
| 260 | + /> |
| 261 | +) |
| 262 | +``` |
| 263 | + |
| 264 | +This is almost ready: all you need to do is export the pure component that you |
| 265 | +are passing to StaticQuery. Rename it first to avoid confusion: |
| 266 | + |
| 267 | +```js |
| 268 | +// src/components/Header.js |
| 269 | +import React from "react" |
| 270 | +import { StaticQuery, graphql } from "gatsby" |
| 271 | + |
| 272 | +export const PureHeader = ({ data }) => ( |
| 273 | + <header> |
| 274 | + <h1>{data.site.siteMetadata.title}</h1> |
| 275 | + </header> |
| 276 | +) |
| 277 | + |
| 278 | +export const Header = props => ( |
| 279 | + <StaticQuery |
| 280 | + query={graphql` |
| 281 | + query { |
| 282 | + site { |
| 283 | + siteMetadata { |
| 284 | + title |
| 285 | + } |
| 286 | + } |
| 287 | + } |
| 288 | + `} |
| 289 | + render={data => <PureHeader data={data} {...props} />} |
| 290 | + /> |
| 291 | +) |
| 292 | + |
| 293 | +export default Header |
| 294 | +``` |
| 295 | + |
| 296 | +Now you have two components exported from the file: the component that includes |
| 297 | +the StaticQuery data which is still the default export, and another component |
| 298 | +that you can test. This means you can test the component independently of the |
| 299 | +GraphQL. |
| 300 | + |
| 301 | +This is a good example of the benefits of keeping components "pure", meaning |
| 302 | +they always generate the same output if given the same inputs and have no |
| 303 | +side-effects apart from their return value. This means we can be sure the tests |
| 304 | +are always reproducable and don't fail if, for example, the network is down or |
| 305 | +the data source changes. In this example, `Header` is impure as it makes a |
| 306 | +query, so the output depends on something apart from its props. `PureHeader` is |
| 307 | +pure because its return value is entirely dependent on the props passed it it. |
| 308 | +This means it's very easy to test, and a snapshot should never change. |
| 309 | + |
| 310 | +Here's how: |
| 311 | + |
| 312 | +```js |
| 313 | +// src/components/Header.test.js |
| 314 | + |
| 315 | +import React from "react" |
| 316 | +import renderer from "react-test-renderer" |
| 317 | +import { PureHeader as Header } from "./Header" |
| 318 | + |
| 319 | +describe("Header", () => |
| 320 | + it("renders correctly", () => { |
| 321 | + // Created using the query from Header.js |
| 322 | + const data = { |
| 323 | + site: { |
| 324 | + siteMetadata: { |
| 325 | + title: "Gatsby Starter Blog", |
| 326 | + }, |
| 327 | + }, |
| 328 | + } |
| 329 | + const tree = renderer.create(<Header data={data} />).toJSON() |
| 330 | + expect(tree).toMatchSnapshot() |
| 331 | + })) |
| 332 | +``` |
| 333 | + |
| 334 | +## Using TypeScript |
| 335 | + |
| 336 | +If you are using TypeScript this is a lot easier to get right as the type errors |
| 337 | +will tell you exaclty what you should be passing to the components. This is why |
| 338 | +it is a good idea to define type interfaces for all of your GraphQL queries. |
0 commit comments