'How to test _document in Next using Jest

I'm trying to achieve 100% coverage in a project and this is the only file I can't test because I haven't got any idea of how to do it.

I don't even know where to start from.

I'm using Jest and React Testing Library. The project uses NextJS.

import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}

ps: I know coverage isn't the most important thing, but for this project 100% is needed.



Solution 1:[1]

Often with NextJS, we need to test 2 cases, the Initial/Server Props part and the React Component part. Yours have only the getInitialProps. Test might differs according to configuration. I'll post my configuration and Tests for both cases for future readers, and hope it can be a solid base to cover most of it at least.

File pages/_document.js

    import React from 'react';
    import Document, { Html, Head, Main, NextScript } from 'next/document';
    import { ServerStyleSheets } from '@material-ui/core/styles';
    
    export default class MyDocument extends Document {
      render() {
        return (
          <Html lang="en">
            <Head>
              <link
                rel="stylesheet"
                href="https://fonts.googleapis.com/css?family=Lato"
              />
            </Head>
            <body>
              <Main />
              <NextScript />
            </body>
          </Html>
        );
      }
    }
    
    MyDocument.getInitialProps = async ctx => {
      // Render app and page and get the context of the page with collected side effects.
      const sheets = new ServerStyleSheets();
      const originalRenderPage = ctx.renderPage;
    
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheets.collect(<App {...props} />),
        });
    
      const initialProps = await Document.getInitialProps(ctx);
    
      return {
        ...initialProps,
        // Styles fragment is rendered after the app and page rendering finish.
        styles: [
          ...React.Children.toArray(initialProps.styles),
          sheets.getStyleElement(),
        ],
      };
    };

File __tests__/pages/_document.js

Before Posting the Test file, one thing that is very important is to Stub the context, ctx in MyDocument.getInitialProps = async ctx => { and mock the ctx.renderPage that will be backed up in document codes and called. The result of this call is another function that also need to be called in other to reach maximum coverage in that section. To get a hint on what to use, you can simply log the ctx inside the document and see wha the function looks like. The stub and mock can be like this:

    const ctx = {
      renderPage: (options = {}) => {
        // for coverage, call enhanceApp and App
        if (typeof options.enhanceApp === 'function') {
          // pass a functional component as parameter
          const app = options.enhanceApp(() => <div>App Rendered</div>);
          app();
        }
        return {
          html: <div>App Rendered</div>,
          head: (
            <head>
              <title>App Title</title>
            </head>
          ),
        };
      },
    };

Here is the full test file, which also handle Shallow Rendering:

    import { createShallow } from '@material-ui/core/test-utils';
    import MockProviders from '../../tests/MockProviders';
    import MyDocument from '../../pages/_document';
    
    /** @test {Document Component getInitialProps} */
    describe('Document Component getInitialProps', () => {
      const ctx = {
        asPath: '/', // not necessary, but useful for testing _app.js
        res: {
          writeHead: jest.fn(),
          end: jest.fn(),
        }, // not necessary but useful for testing other files
        renderPage: (options = {}) => {
          // for coverage, call enhanceApp and App
          console.log('options', options);
          if (typeof options.enhanceApp === 'function') {
            const app = options.enhanceApp(() => <div>App Rendered</div>);
            console.log('app', app);
            app();
          }
          return {
            html: <div>App Rendered</div>,
            head: (
              <head>
                <title>App Title</title>
              </head>
            ),
          };
        },
      };
    
      it('should return finalize html, head and styles in getInitialProps', async () => {
        const result = await MyDocument.getInitialProps(ctx);
        // Log to see the structure for your assertion if any expectation
        // console.log(result);
        expect(result.html.props.children).toBe('App Rendered');
        expect(result.head.props.children.props.children).toBe('App Title');
        expect(result.styles[0].props.id).toBe('jss-server-side');
      });
    });
    
    /** @test {Document Component} */
    describe('Document Component', () => {
      const shallow = createShallow();
      const wrapper = shallow(
        <MockProviders>
          <MyDocument />
        </MockProviders>
      );
    
      const comp = wrapper.find('MyDocument').dive();
      // console.log(comp.debug());
    
      it('should render Document components Html', () => {
        expect(comp.find('Html')).toHaveLength(1);
        expect(comp.find('Head')).toHaveLength(1);
        expect(comp.find('body')).toHaveLength(1);
        expect(comp.find('Main')).toHaveLength(1);
        expect(comp.find('NextScript')).toHaveLength(1);
      });
    });

EDIT 1------- My MockProviders file is just for code factorization, instead of adding Providers components in cascade on each test, and later to change all test files if you need to add another Provider, then you would only to change that one MockProvider file. It's a king of mocking itself because you inject your own props on it while testing, which is different from the normal value you might inject on the real application.

    import { MuiThemeProvider } from '@material-ui/core/styles';
    import { StateProvider } from '../src/states/store';
    import theme from '../src/themes';
    
    const MockProviders = props => {
      return (
        <StateProvider {...props}>
          <MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>
        </StateProvider>
      );
    };
    
    export default MockProviders;

Because I used a Provider for managing state with React.useContext and a Provider for MaterialUI theme, then I add both of them in cascade, with ability to pass additional props, and render the children components inside.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Florian Falk