【问题标题】:Diagnosing duplicate spec reporting with Karma, Mocha, and React 16.5使用 Karma、Mocha 和 React 16.5 诊断重复的规范报告
【发布时间】:2018-09-07 14:37:41
【问题描述】:

我有一个项目使用 React 作为视图层。为了测试它,我使用 mocha、karma、karma-webpack 等。出于某种原因,在 React 16+ 中,karma 报告 afterEach 已针对两个规范运行了 3 次。这在 React 16+ 中发生,process.env.NODE_ENVdevelopment不是 production 时发生。

在之前对该问题的探索中,规范失败的原因可能会级联并污染后续规范。为了帮助确定根本原因,这是我能找到的最简单的示例。

我试图追踪这种行为,但被业力和套接字内部和周围的复杂性所困扰。考虑下面的示例,目前可在https://github.com/jneander/react-mocha 获得。

Example.js

import React, {Component} from 'react'

export default class Example extends Component {
  render() {
    try {
      return (
        <div>{this.props.foo.bar}</div>
      )
    } catch(e) {
      console.log(e) // for logging purposes
      throw e
    }
  }
}

Example.spec.js

import {expect} from 'chai'
import React from 'react'
import ReactDOM from 'react-dom'

class ExampleWrapper extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      error: false
    }
  }

  componentDidCatch(error) {
    console.log('there was a problem')
    this.setState({
      error: true
    })
  }

  render() {
    console.log('rendering!')
    if (this.state.error) {
      console.log('- rendering the error version')
      return <div>An error occurred during render</div>
    }

    console.log('- rendering the real version')
    return (
      <Example {...this.props} />
    )
  }
}

import Example from './Example'

describe('Example', () => {
  let $container

  beforeEach(() => {
    console.log('beforeEach')
    $container = document.createElement('div')
    document.body.appendChild($container)
  })

  afterEach(() => {
    console.log('afterEach')
    ReactDOM.unmountComponentAtNode($container)
    $container.remove()
  })

  async function mount(props) {
    return new Promise((resolve, reject) => {
      const done = () => {
        console.log('done rendering')
        resolve()
      }
      ReactDOM.render(<ExampleWrapper {...props} />, $container, done)
    })
  }

  it('fails this spec', async () => {
    console.log('start test 1')
    await mount({})
    expect(true).to.be.true
  })

  it('also fails, but because of the first spec', async () => {
    console.log('start test 2')
    await mount({foo: {}})
    expect(true).to.be.true
  })
})

规范输出如下:

LOG LOG: 'beforeEach'
LOG LOG: 'start test 1'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the real version'

  Example
    ✗ fails this spec
  Error: Uncaught TypeError: Cannot read property 'bar' of undefined (src/Example.spec.js:35380)
      at Object.invokeGuardedCallbackDev (src/Example.spec.js:16547:16)
      at invokeGuardedCallback (src/Example.spec.js:16600:31)
      at replayUnitOfWork (src/Example.spec.js:31930:5)
      at renderRoot (src/Example.spec.js:32733:11)
      at performWorkOnRoot (src/Example.spec.js:33572:7)
      at performWork (src/Example.spec.js:33480:7)
      at performSyncWork (src/Example.spec.js:33452:3)
      at requestWork (src/Example.spec.js:33340:5)
      at scheduleWork (src/Example.spec.js:33134:5)

ERROR LOG: 'The above error occurred in the <Example> component:
    in Example (created by ExampleWrapper)
    in ExampleWrapper

React will try to recreate this component tree from scratch using the error boundary you provided, ExampleWrapper.'
LOG LOG: 'there was a problem'
LOG LOG: 'done rendering'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the error version'
LOG LOG: 'afterEach'
LOG LOG: 'beforeEach'
LOG LOG: 'start test 2'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the real version'
LOG LOG: 'done rendering'
    ✓ also fails, but because of the first spec
    ✓ also fails, but because of the first spec
LOG LOG: 'afterEach'
LOG LOG: 'afterEach'

Chrome 69.0.3497 (Mac OS X 10.13.6): Executed 3 of 2 (1 FAILED) (0.014 secs / NaN secs)
TOTAL: 1 FAILED, 2 SUCCESS


1) fails this spec
     Example
     Error: Uncaught TypeError: Cannot read property 'bar' of undefined (src/Example.spec.js:35380)
    at Object.invokeGuardedCallbackDev (src/Example.spec.js:16547:16)
    at invokeGuardedCallback (src/Example.spec.js:16600:31)
    at replayUnitOfWork (src/Example.spec.js:31930:5)
    at renderRoot (src/Example.spec.js:32733:11)
    at performWorkOnRoot (src/Example.spec.js:33572:7)
    at performWork (src/Example.spec.js:33480:7)
    at performSyncWork (src/Example.spec.js:33452:3)
    at requestWork (src/Example.spec.js:33340:5)
    at scheduleWork (src/Example.spec.js:33134:5)

导致重复报告的原因是什么?

为什么这会发生在 React 16+ 而不是 React 15?

我该如何解决这个问题?

【问题讨论】:

  • 我在最近的努力中观察到的是,Mocha 承认从测试函数返回了一个 Promise,但是并没有等待它真正解决,然后继续使用该套件的其余部分。这只会在使用 React 渲染时发生,而不是在返回一个简单的、带有 setTimeout 的原生 Promise 时。

标签: reactjs mocha.js karma-runner karma-mocha react-16


【解决方案1】:

可能存在竞争条件,因为使用ref 函数解决了一个promise。收到组件 ref 并不意味着初始渲染已经完成。

正如the reference 所说,

如果您需要对根 ReactComponent 实例的引用,首选的解决方案是将回调 ref 附加到根元素。

解决promise的正确方法是使用render回调参数,

如果提供了可选的回调,它将在组件渲染或更新后执行。

应该是:

async function mount(props) {
  return new Promise(resolve => {
    ReactDOM.render(<Example {...props} />, $container, resolve)
  })
}

该问题不会出现在第二次测试中,无论是否有第二次测试,它都会出现在第一次测试中,并且不是特定于 React 16.5。 它特定于 React 开发模式的工作原理

这里是a simplified demo,不包括摩卡因素。预期的错误是 console.warn 输出,但两个 Error: Cannot read property 'bar' of undefined 错误是 console.error,它们是由 React 本身输出的。 ReactDOM.render 运行组件render 函数两次,并从第一次测试异步输出错误。

The same demo 与 React 的生产版本同步显示单个 Error: Cannot read property 'bar' of undefined 错误,正如预期的那样。渲染失败不会使ReactDOM 渲染被拒绝,错误可能是caught by error boundary component if needed

class EB extends Component {
  componentDidCatch(err) {
    this.props.onCatch(err);
  }

  render() {
    return this.props.children;
  }
}

async function mount(props) {
  return new Promise((resolve, reject) => {
    ReactDOM.render(<EB onCatch={reject}><Example {...props} /></EB>, $container, resolve)
  })
}

最好不要在单元测试中依赖 React DOM 渲染器。 Enzyme 服务于这个目的,允许独立地同步测试组件,尤其是shallow wrapper

【讨论】:

  • enzyme 的使用是一个漫长而独立的对话。这不是一种选择,因为它不能充分地执行被测代码。为了与真实用户条件保持一致,首选 ReactDOM。除此之外,将 promise resolve 回调移动到 ReactDOM.render 回调似乎确实解决了这个问题。我以为我以前尝试过,没有任何变化,但一定是弄错了。我用这个小金块尝试了很多不同的东西。
  • 后续:进行该更改后,afterEach 回调运行了 3 次,导致规范计数不准确。其他问题可能存在,但被掩盖了。知道为什么这个特定的例子会发生这种情况吗?
  • @jneander 与真实用户条件的一致性通常应该在黑盒 e2e 测试中进行测试,除了应用程序的 React 特性。单元测试是孤立地测试单元并缩小可能出现的问题。这就是酶通常起作用的原因。如果您关心酶,请考虑问另一个问题,您的问题可能可以通过常规方式解决。无论如何,使用 React 的生产构建将更接近真实情况,并且可能需要捕获渲染错误。我用错误边界示例更新了答案。
  • 在这种情况下,我看不出任何 afterEach 问题的原因,至少从显示的代码来看。是否知道 beforeEach 运行 2 次和 afterEach 运行 3 次?你是怎么测试的?
  • beforeEachafterEach 中的console.log 语句都很好。 beforeEach 被击中两次。 afterEach 被击中三次。结果,mocha 似乎被欺骗了,认为有三个测试而不是两个。
【解决方案2】:

似乎 React 16+ 在渲染过程中会出现未捕获的错误,即使在包装器中使用 componentDidCatch 也是如此。这意味着 Mocha 将因未捕获的错误而使测试失败,然后继续下一个测试,之后组件的第二次渲染将完成并解决 promise,运行一个断言。这在当前正在进行的测试中运行,导致本示例中看到的双重成功。

An issue 已通过 Github 上的 React 存储库打开。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-26
    • 2017-10-31
    • 1970-01-01
    相关资源
    最近更新 更多