首页 > 解决方案 > 如何使用 Jest 在 Next 中测试 _document

问题描述

我试图在一个项目中实现 100% 的覆盖率,这是我无法测试的唯一文件,因为我不知道如何去做。

我什至不知道从哪里开始。

我正在使用 Jest 和 React 测试库。该项目使用 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:我知道覆盖率不是最重要的,但对于这个项目来说,100% 是必要的。

标签: testingjestjsnext.js

解决方案


通常使用 NextJS,我们需要测试 2 个案例,Initial/Server Props 部分和 React 组件部分。你的只有getInitialProps. 测试可能因配置而异。我将为未来的读者发布这两种情况的配置和测试,并希望它可以成为至少涵盖大部分内容的坚实基础。

文件 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(),
    ],
  };
};

文件 __tests__/pages/_document.js

在发布测试文件之前,非常重要的事情是存根上下文,ctxMyDocument.getInitialProps = async ctx => {模拟ctx.renderPage将在文档代码中备份并调用的内​​容。此调用的结果是另一个函数,也需要在 other 中调用以达到该部分的最大覆盖率。要获得使用什么的提示,您可以简单地在文档中记录 ctx 并查看该函数的外观。存根和模拟可以是这样的:

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>
      ),
    };
  },
};

这是完整的测试文件,它也处理浅渲染:

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);
  });
});

推荐阅读