Диагностика повторяющихся отчетов о спецификациях с помощью Karma, Mocha и React 16.5

У меня есть проект, использующий React для слоя представления. Чтобы проверить это, я использую mocha, karma, karma-webpack и т. д. По какой-то причине в React 16+ карма сообщает, что afterEach выполнялся три раза для двух спецификаций. Это происходит только в React 16+ и только, когда process.env.NODE_ENV равно development, а не production.

В предыдущих исследованиях этой проблемы причины сбоев спецификации могли каскадироваться и загрязнять последующие спецификации. Чтобы помочь определить основную причину, это самый простой пример, который я могу найти.

Я пытался проследить поведение, но был поставлен в тупик из-за сложности внутри и вокруг кармы и сокетов. Рассмотрим приведенный ниже пример, доступный на данный момент по адресу https://github.com/jneander/react-mocha<. /а>.

Пример.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
    }
  }
}

Пример.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?

Как я могу решить эту проблему?


person jneander    schedule 07.09.2018    source источник
comment
Что я наблюдаю с последними усилиями, так это то, что Mocha признает, что обещание возвращается из тестовой функции, но затем не ждет, пока оно действительно разрешится, прежде чем продолжить работу с остальной частью пакета. Это происходит только при рендеринге с помощью React, а не при возврате простого нативного промиса с setTimeout внутри.   -  person jneander    schedule 17.09.2018


Ответы (2)


Могут быть условия гонки, потому что обещание разрешается с помощью функции ref. Получение ссылки на компонент не означает, что первоначальный рендеринг завершен.

Как указано в справке,

Если вам нужна ссылка на корневой экземпляр ReactComponent, предпочтительным решением является присоединение ссылки обратного вызова к корневому элементу.

Правильный способ разрешить промис — использовать параметр обратного вызова render,

Если указан необязательный обратный вызов, он будет выполнен после рендеринга или обновления компонента.

Так должно быть:

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

Проблема не возникает во втором тесте, она возникает в первом тесте независимо от того, есть ли второй тест, и не относится к React 16.5. Это зависит от того, как работает режим разработки React.

Вот упрощенная демонстрация, которая исключает фактор Mocha. Ожидаемые ошибки — это console.warn вывод, но две ошибки Error: Cannot read property 'bar' of undefined — это console.error, которые выводятся самим React. ReactDOM.render дважды запускает функцию компонента render и асинхронно выводит ошибку из первого теста.

Та же демонстрация с производственной сборкой React показывает единственную ошибку Error: Cannot read property 'bar' of undefined синхронно, как и следовало ожидать. Неудачный рендеринг не приводит к отклонению ReactDOM рендеринга, ошибка может быть обнаружена граничным компонентом ошибки, если это необходимо:

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оболочка в конкретный.

person Estus Flask    schedule 07.09.2018
comment
Использование enzyme — это длинный отдельный разговор. Это не вариант, так как тестируемый код недостаточно тренируется. Для согласованности с реальными пользовательскими условиями предпочтительнее использовать ReactDOM. Кроме того, перемещение обратного вызова promise resolve в обратный вызов ReactDOM.render, похоже, решает проблему. Я думал, что пробовал это раньше без изменений, но, должно быть, ошибся. Я пробовал много разных вещей с этим маленьким самородком. - person jneander; 07.09.2018
comment
Последующие действия: после внесения этого изменения обратный вызов afterEach выполняется три раза, что приводит к неточному подсчету спецификаций. Другие проблемы могут присутствовать, но замаскированы. Любая идея относительно того, почему это может происходить с этим конкретным примером? - person jneander; 07.09.2018
comment
@jneander Согласованность с реальными пользовательскими условиями обычно следует проверять в тестах e2e черного ящика, помимо природы React приложения. В то время как модульные тесты предназначены для тестирования модулей изолированно и сужения возможных проблем. Вот почему Enzyme обычно работает. Попробуйте задать еще один вопрос, если вы обеспокоены Enzyme, ваши проблемы могут быть решены обычным способом. В любом случае, использование продакшн-сборки React будет ближе к реальным условиям, и может быть желательно отлавливать ошибки рендеринга. Я обновил ответ примером границы ошибки. - person Estus Flask; 07.09.2018
comment
Я не вижу никаких причин для проблемы afterEach в этом случае, по крайней мере, из приведенного кода. Известно ли, что beforeEach выполняется 2 раза, а afterEach 3 раза? Как вы это проверили? - person Estus Flask; 07.09.2018
comment
Старые добрые операторы console.log как в beforeEach, так и в afterEach. beforeEach попал дважды. afterEach попал трижды. В результате мокко кажется обманутым, заставив думать, что есть три теста вместо двух. - person jneander; 07.09.2018
comment
Трудно поверить, что такое может случиться с Mocha, я не знаю условий, при которых afterEach мог бы работать без фактического тестирования. Более вероятным сценарием является то, что пакет запускается дважды, но по какой-то причине рано завершается сбоем или что-то в этом роде. Убедитесь, что вы сначала запустили console.log, потому что, если блок выйдет из строя, подсчет будет неточным. - person Estus Flask; 07.09.2018
comment
Давайте продолжим это обсуждение в чате. - person jneander; 07.09.2018
comment
У меня нет этому объяснений. Можете ли вы предоставить способ репликации пробема, репо и т. д.? - person Estus Flask; 07.09.2018
comment
Я загрузил этот код на github.com/jneander/react-mocha, но не буду его хранить репо вокруг после того, как мы сможем добраться до сути этого. Посмотрите и посмотрите, сможете ли вы сделать из этого орел или решку. - person jneander; 07.09.2018
comment
Конечно. Я понимаю. Вы использовали сборку React dev. Это не происходит с prod build. Я не уверен, что там происходит, неясно, происходит ли проблема в Карме или Мокке. Вероятно, это как-то связано с тем, как React обрабатывает ошибки. Он устанавливает глобальный прослушиватель событий error, это, вероятно, сквозная проблема. Используйте производственную сборку, и я бы посоветовал придерживаться Enzyme для более компактных модульных тестов, потому что это помогает избежать глупых ситуаций, подобных этой. - person Estus Flask; 08.09.2018
comment
enzyme является нарушителем условий сделки, так как он предотвращает какое-либо поведение, полностью имитируя его. Использование ReactDOM максимально приближает нас к производственной среде. Присутствующая здесь проблема появляется только после React 15.6.2 и только с process.env.NODE_ENV === 'development'. Было бы предпочтительнее иметь возможность обойти это поведение без обфускации, которая идет с производственным кодом. - person jneander; 08.09.2018
comment
Кстати, ваша помощь очень ценна. Различие между производством и разработкой, вероятно, никогда бы не пришло мне в голову. - person jneander; 08.09.2018
comment
Как я уже упоминал, обычно все тесты, которые проверяют реальное поведение, выполняются как тесты черного ящика E2E (Protractor/Testcafe, а не Mocha), поэтому эта проблема не существует для 99% разработчиков, включая меня. Да, это изменения React 16, похоже, они как-то связаны с волокнами. Я тоже не заметил разницы, но тут она очевидна. - person Estus Flask; 08.09.2018
comment
Я бы порекомендовал изолировать проблему (возможно, ее можно воссоздать с помощью Mocha только с JSDOM) и сообщить о ней в репозиторий React. И React, и среда тестирования, кажется, делают что-то хакерское с обработкой ошибок, но, поскольку React не должен быть хакерским, я бы винил его в этом. Кстати, в репозитории были ошибки, он предъявлял требования к глобальной Karma и Chrome, также были проблемы с конфигурацией Karma, которые мешали его запуску. И нет никакого globals.js. Пришлось отключить исходные карты и переключиться на Puppeteer. - person Estus Flask; 08.09.2018

Кажется, что React 16+ выдает необнаруженную ошибку во время рендеринга, даже при использовании componentDidCatch в оболочке. Это означает, что Mocha провалит тест с неперехваченной ошибкой, затем продолжит следующий тест, после чего второй рендеринг компонента завершится и разрешит промис, выполнив утверждение. Это выполняется в рамках текущего теста, что приводит к двойному успеху, наблюдаемому в этом примере.

Открыта проблема с репозиторием React на Github.

person jneander    schedule 18.09.2018