diff --git a/docs/docs/testing-components-with-graphql.md b/docs/docs/testing-components-with-graphql.md new file mode 100644 index 0000000000000..d75ef4bd7dbb7 --- /dev/null +++ b/docs/docs/testing-components-with-graphql.md @@ -0,0 +1,338 @@ +--- +Title: Testing components with GraphQL +--- + +If you try to run unit tests on components that use GraphQL queries, you will +discover that you have no data. Jest can't run your queries, so if you are +testing components that rely on GraphQL data, you will need to provide the data +yourself. This is a good thing, as otherwise your tests could break if your data +changes, and in the case of remote data sources it would need network access to +run tests. + +In general it is best practice to test the smallest components possible, so the +simplest thing to do is to test the individual page components with mock data, +rather than trying to test a full page. However, if you do want to test the full +page you'll need to provide the equivalent data to the component. Luckily +there's a simple way to get the data you need. + +First you should make sure you have read +[the unit testing guide](/docs/unit-testing/) and set up your project as +described. This guide is based on the same blog starter project. You will be +writing a simple snapshot test for the index page. + +As Jest doesn't run or compile away your GraphQL queries you need to mock the +`graphql` function to stop it throwing an error. If you set your project up with +a mock for `gatsby` as described in the unit testing guide then this is already +done. + +## Testing page queries + +As this is testing a page component you will need to put your tests in another +folder so that Gatsby doesn't try to turn the tests into pages. + +```js +// src/__tests__/index.js + +import React from "react" +import renderer from "react-test-renderer" +import BlogIndex from "../pages/index" + +describe("BlogIndex", () => + it("renders correctly", () => { + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +If you run this test you will get an error, as the component is expecting a +location object. You can fix this by passing one in: + +```js +// src/__tests__/index.js + +import React from "react" +import renderer from "react-test-renderer" +import BlogIndex from "../pages/index" + +describe("BlogIndex", () => + it("renders correctly", () => { + const location = { + pathname: "", + } + + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +This should fix the `location` error, but now you will have an error because +there is no GraphQL data being passed to the component. We can pass this in too, +but the structure is a little more complicated. Luckily there's an easy way to +get some suitable data. Run `gatsby develop` and go to +http://localhost:8000/___graphql to load the GraphiQL IDE. You can now get the +right data using the same query that you used on the page. If it is a simple +query with no fragments you can copy it directly. That is the case here, run +this query copied from the index page: + +```graphql +query IndexQuery { + site { + siteMetadata { + title + } + } + allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) { + edges { + node { + excerpt + fields { + slug + } + frontmatter { + date(formatString: "DD MMMM, YYYY") + title + } + } + } + } +} +``` + +The output panel should now give you a nice JSON object with the query result. +Here it is, trimmed to one node for brevity: + +```json +{ + "data": { + "site": { + "siteMetadata": { + "title": "Gatsby Starter Blog" + } + }, + "allMarkdownRemark": { + "edges": [ + { + "node": { + "excerpt": + "Far far away, behind the word mountains, far from the countries Vokalia and\nConsonantia, there live the blind texts. Separated they live in…", + "fields": { + "slug": "/hi-folks/" + }, + "frontmatter": { + "date": "28 May, 2015", + "title": "New Beginnings" + } + } + } + ] + } + } +} +``` + +GraphiQL doesn't know about any fragments defined by Gatsby, so if your query +uses them then you'll need to replace those with the content of the fragment. If +you're using `gatsby-transformer-sharp` you'll find the fragments in +[gatsby-transformer-sharp/src/fragments.js](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-transformer-sharp/src/fragments.js). +So, for example if your query includes: + +```graphql + image { + childImageSharp { + fluid(maxWidth: 1024) { + ...GatsbyImageSharpFluid + } + } + } +``` + +...it becomes: + +```graphql + image { + childImageSharp { + fluid(maxWidth: 1024) { + base64 + aspectRatio + src + srcSet + sizes + } + } + } +``` + +When you have the result, copy the `data` value from the output panel. Good +practice is to store your fixtures in a separate file, but for simplicity here +you will be defining it directly inside your test file: + +```js +// src/__tests__/index.js + +import React from "react" +import renderer from "react-test-renderer" +import BlogIndex from "../pages/index" + +describe("BlogIndex", () => + it("renders correctly", () => { + const location = { + pathname: "", + } + + const data = { + site: { + siteMetadata: { + title: "Gatsby Starter Blog", + }, + }, + allMarkdownRemark: { + edges: [ + { + node: { + excerpt: + "Far far away, behind the word mountains, far from the countries Vokalia and\nConsonantia, there live the blind texts. Separated they live in…", + fields: { + slug: "/hi-folks/", + }, + frontmatter: { + date: "28 May, 2015", + title: "New Beginnings", + }, + }, + }, + ], + }, + } + + const tree = renderer + .create() + .toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +Run the tests and they should now pass. Take a look in `__snapshots__` to see +the output. + +## Testing StaticQuery + +The method above works for page queries, as you can pass the data in directly to +the component. This doesn't work for components that use `StaticQuery` though, +as that uses `context` rather than `props` so we need to take a slightly +different approach to testing these. The blog starter project doesn't include +`StaticQuery`, so the example here is from +[the StaticQuery docs](/docs/static-query/). + +Using `StaticQuery` allows you to make queries in any component, not just pages. +This gives a lot of flexibility, and avoid having to pass the props down to +deeply-nested components. The pattern for enabling type checking described in +the docs is a good starting point for making these components testable, as it +separates the query from the definition of the component itself. However that +example doesn't export the inner, pure component, which is what you'll need to +test. + +Here is the example of a header component that queries the page data itself, +rather than needing it to be passed from the layout: + +```js +// src/components/Header.js +import React from "react" +import { StaticQuery } from "gatsby" + +const Header = ({ data }) => ( +
+

{data.site.siteMetadata.title}

+
+) + +export default props => ( +
} + /> +) +``` + +This is almost ready: all you need to do is export the pure component that you +are passing to StaticQuery. Rename it first to avoid confusion: + +```js +// src/components/Header.js +import React from "react" +import { StaticQuery, graphql } from "gatsby" + +export const PureHeader = ({ data }) => ( +
+

{data.site.siteMetadata.title}

+
+) + +export const Header = props => ( + } + /> +) + +export default Header +``` + +Now you have two components exported from the file: the component that includes +the StaticQuery data which is still the default export, and another component +that you can test. This means you can test the component independently of the +GraphQL. + +This is a good example of the benefits of keeping components "pure", meaning +they always generate the same output if given the same inputs and have no +side-effects apart from their return value. This means we can be sure the tests +are always reproducable and don't fail if, for example, the network is down or +the data source changes. In this example, `Header` is impure as it makes a +query, so the output depends on something apart from its props. `PureHeader` is +pure because its return value is entirely dependent on the props passed it it. +This means it's very easy to test, and a snapshot should never change. + +Here's how: + +```js +// src/components/Header.test.js + +import React from "react" +import renderer from "react-test-renderer" +import { PureHeader as Header } from "./Header" + +describe("Header", () => + it("renders correctly", () => { + // Created using the query from Header.js + const data = { + site: { + siteMetadata: { + title: "Gatsby Starter Blog", + }, + }, + } + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +## Using TypeScript + +If you are using TypeScript this is a lot easier to get right as the type errors +will tell you exaclty what you should be passing to the components. This is why +it is a good idea to define type interfaces for all of your GraphQL queries. diff --git a/docs/docs/unit-testing.md b/docs/docs/unit-testing.md new file mode 100644 index 0000000000000..de31eb3c3f5ae --- /dev/null +++ b/docs/docs/unit-testing.md @@ -0,0 +1,327 @@ +--- +title: Unit testing +--- + +Unit testing is a great way to protect against errors in your code before you +deploy it. While Gatsby does not include support for unit testing out of the +box, it only takes a few steps to get up and running. However there are a few +features of the Gatsby build process that mean the standard Jest setup doesn't +quite work. This guide shows you how to set it up. + +## Setting up your environment + +The most popular testing framework for React is [Jest](https://jestjs.io/), +which was created by Facebook. While Jest is a general purpose JavaScript unit +testing framework, it has lots of features that make it work particularly well +with React. + +For this guide, you will be starting with `gatsby-starter-blog`, but the +concepts should be the same or very similar for your site. + +First you need to install Jest and some more required packages. You need to +install Babel 7 as it's required by Jest. + +```sh +npm install --save-dev jest babel-jest react-test-renderer identity-obj-proxy 'babel-core@^7.0.0-0' @babel/core @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties @babel/plugin-proposal-optional-chaining +``` + +Because Gatsby handles its own Babel configuration, you will need to manually +tell Jest to use `babel-jest`. The easiest way to do this is to add a `"jest"` +section in your `package.json`. You can set up some useful defaults at the same +time: + +```json + "jest": { + "transform": { + "^.+\\.jsx?$": "/jest-preprocess.js" + }, + "testRegex": "/.*(__tests__\\/.*)|(.*(test|spec))\\.jsx?$", + "moduleNameMapper": { + ".+\\.(css|styl|less|sass|scss)$": "identity-obj-proxy", + ".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js" + }, + "testPathIgnorePatterns": ["node_modules", ".cache"], + "transformIgnorePatterns": [ + "node_modules/(?!(gatsby)/)" + ], + "globals": { + "__PATH_PREFIX__": "" + }, + "testURL": "http://localhost", + "setupFiles": [ + "/loadershim.js" + ] + } +``` + +The `transform` section tells Jest that all `js` or `jsx` files need to be +transformed using a `jest-preprocess.js` file in the project root. Go ahead and +create this file now. This is where you set up your Babel config. You can start +with a minimal config. + +```js +// jest-preprocess.js +const babelOptions = { + presets: ["@babel/react", "@babel/env"], + plugins: [ + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-class-properties", + ], +} + +module.exports = require("babel-jest").createTransformer(babelOptions) +``` + +Back to the Jest config, you can see the next option is `testRegex`. This is the +pattern telling Jest which files contain tests. The pattern above matches any +`.js` file inside a `__tests__` directory, or any file elsewhere with the +extension `.test.js` or `.spec.js`. You are telling Jest to ignore any tests in +the `node_modules` or `.cache` directories. + +The `moduleNameMapper` section works a bit like webpack rules, and tells Jest +how to handle imports. You are mainly concerned here with mocking static file +imports, which Jest can't handle. A mock is a dummy module that is used instead +of the real module inside tests. It is good when you have something that you +can't or don't want to test. You can mock anything, and here you are mocking +assets rather than code. For stylesheets you need to use the package +`identity-obj-proxy`. For all other assets you need to use a manual mock called +`fileMock.js`. You need to create this yourself. The convention is to create a +directory called `__mocks__` in the root directory for this. Note the pair of +double underscores in the name. + +```js +// __mocks__/fileMock.js +module.exports = "test-file-stub" +``` + +The next config setting is `transformIgnorePatterns`. This is very important, +and is different from what you'll find in other Jest guides. The reason that you +need this is because Gastby includes un-transpiled ES6 code. By default Jest +doesn't try to transform code inside `node_modules`, so you will get an error +like this: + +``` +/my-blog/node_modules/gatsby/cache-dir/gatsby-browser-entry.js:1 +({"Object.":function(module,exports,require,__dirname,__filename,global,jest){import React from "react" + ^^^^^^ +SyntaxError: Unexpected token import +``` + +This is because `gatsby-browser-entry.js` isn't being transpiled before running +in Jest. You can fix this by changing the default `transformIgnorePatterns` to +exclude the `gatsby` module. + +The `globals` section sets `__PATH_PREFIX__`, which is usually set by Gatsby, +and which some components need. + +You need to set `testURL` to a valid URL, because some DOM APIs such as +`localStorage` are unhappy with the default (`about:blank`). + +There's one more global that you need to set, but as it's a function you can't +set it here in the JSON. The `setupFiles` array lets you list files that will be +included before all tests are run, so it's perfect for this. + +```js +// loadershim.js + +global.___loader = { + enqueue: jest.fn(), +} +``` + +Finally it's a good idea to mock the gatsby module itself. This may not be +needed at first, but will make things a lot easier if you want to test +components that use `Link` or GraphQL. + +```js +// __mocks__/gatsby.js +const gatsby = jest.requireActual("gatsby") +module.exports = { ...gatsby, graphql: jest.fn(), Link: "Link" } +``` + +This mocks the `graphql()` function and `Link` component. + +## Writing tests + +A full guide to unit testing is beyond the scope of this guide, but you can +start with a simple snapshot test to check that everything is working. + +First, create the test file. You can either put these in a `__tests__` +directory, or put them elsewhere (usually next to the component itself), with +the extension `.spec.js` or `.test.js`. The decision comes down to your own +taste. For this guide you will be testing the `` component, so create a +`Bio.test.js` file next to it in `src/components`: + +```js +import React from "react" +import renderer from "react-test-renderer" +import Bio from "./Bio" + +describe("Bio", () => + it("renders correctly", () => { + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +This is a very simple snapshot test, which uses `react-test-renderer` to render +the component, and then generates a snapshot of it on first run. It then +compares future snapshots against this, which means you can quickly check for +regressions. Visit [the Jest docs](https://jestjs.io/docs/en/getting-started) to +learn more about other tests that you can write. + +## Running tests + +If you look inside `package.json` you will probably find that there is already a +script for `test`, which just outputs an error message. Change this to simply +`jest`: + +```json + "scripts": { + "test": "jest" + } +``` + +This means you can now run tests by typing `npm run test`. If you want you could +also add a script that runs `jest --watchAll` to watch files and run tests when +they are changed. + +Now, run `npm run test` and you should immediately get an error like this: + +```sh + @font-face { + ^ + + SyntaxError: Invalid or unexpected token + + 2 | + 3 | // Import typefaces + > 4 | import 'typeface-montserrat' +``` + +This is because the CSS mock doesn't recognize the `typeface-` modules. You can +fix this easily by creating a new manual mock. Back in the `__mocks__` +directory, create a file called `typeface-montserrat.js` and another called +`typeface-merriweather.js`, each with the content `{}`. Any file in the mocks +folder which has a name that matches that of a node_module is automatically used +as a mock. + +Run the tests again now and it should all work! You should get a message about +the snapshot being written. This is created in a `__snapshots__` directory next +to your tests. If you take a look at it, you will see that it is a JSON +representation of the `` component. You should check your snapshot files +into your SCM repository so that so that any changes are tracked in history. +This is particularly important to remember if you are using a continuous +integration system such as Travis to run tests, as these will fail if no +snapshot is present. + +If you make changes that mean you need to update the snapshot, you can do this +by running `npm run test -- -u`. + +## Using TypeScript + +If you are using TypeScript, you need to make a couple of small changes to your +config. First install `ts-jest`: + +```sh +npm install --save-dev ts-jest +``` + +Then edit the Jest config in your `package.json` to match this: + +```json + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest", + "^.+\\.jsx?$": "/jest-preprocess.js" + }, + "testRegex": "(/__tests__/.*\\.([tj]sx?)|(\\.|/)(test|spec))\\.([tj]sx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "testPathIgnorePatterns": ["node_modules", ".cache"], + "transformIgnorePatterns": [ + "node_modules/(?!(gatsby)/)" + ], + "globals": { + "__PATH_PREFIX__": "" + }, + "testURL": "http://localhost", + "setupFiles": [ + "/loadershim.js" + ] + } +``` + +## Testing components with Router + +When you test components they are not in a `Router`, meaning they don't have +access to some context and props that they may be expecting. The most common of +these is the `Link` component. In the example above we mock the `Link` component +as a string, which is the simplest solution and works for most uses. However +sometimes you might want to test with the real `Link` component. As of v2, +Gatsby uses `@reach/router` for navigation, which is good at handling test +environments, and unlike React Router is happy to render `Link`s outside of a +`Router` context. However there is a small issue related to the `gatsby` mock. +We can use a small workaround to avoid an error. + +First, remove the `Link` mock from `gatsby`: + +```js +// __mocks__/gatsby.js +const gatsby = jest.requireActual("gatsby") +module.exports = { ...gatsby, graphql: jest.fn() } +``` + +While the `Link` component is exported by the main `gatsby` package, it is +actually defined in `gatsby-link`. That in turn uses `parsePath()` from +`gatsby`, which causes module resolution issues. Fortunately it's an easy fix. +You need to create a mock for `gatsby-link`, even though it will actually be the +real module. You do this so that you can tell it to not try and use the mock +`gatsby`: + +```js +// __mocks__/gatsby-link.js +jest.unmock("gatsby") +module.exports = jest.requireActual("gatsby-link") +``` + +One more issue that you may encounter is that some components expect to be able +to use the `location` prop that is passed in by `Router`. You can fix this by +manually passing in the prop: + +```js +// src/__tests__/index.js + +import React from "react" +import renderer from "react-test-renderer" +import BlogIndex from "../pages/index" + +describe("BlogIndex", () => + it("renders correctly", () => { + const location = { + pathname: "/", + } + + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + })) +``` + +For more information on testing page components, be sure to read the docs on +[testing components with GraphQL](/docs/testing-components-with-graphql/) + +## Other resources + +If you need to make changes to your Babel config, you can edit the config in +`jest-preprocess.js`. You may need to enable some of the plugins used by Gatsby, +though remember you may need to install the Babel 7 versions. See +[the Gatsby Babel config guide](/docs/babel/) for some examples. + +For more information on Jest testing, visit +[the Jest site](https://jestjs.io/docs/en/getting-started).