Смоделируйте несколько вызовов извлечения с обновлениями состояния в ReactJS - PullRequest
0 голосов
/ 04 марта 2019

У меня есть компонент ReactJS, который выполняет две функции: - в ComponentDidMount он получит список записей - при нажатии кнопки он отправит выбранную запись в бэкэнд

Проблема в том, что мне нужносмоделируйте оба запроса (сделанные с fetch), чтобы проверить это должным образом.В моем текущем тестовом примере я хочу проверить сбой при отправке по нажатию кнопки.Однако по какой-то странной причине срабатывает setState, однако после того, как я хочу сравнить его, получено обновление от него.

Дампы, которые я сделал для теста.Первый - это состояние, которое слушают в тесте.Второй из самого кода, где он устанавливает состояние (). Error к ошибке, полученной от вызова

FAIL  react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console

  console.log react/src/components/Authentication/DealerSelection.test.jsx:114
    { loading: true,
      error: null,
      options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
  console.log react/src/components/Authentication/DealerSelection.jsx:52
    set error to: my error

Фактический тестовый код:

it('throws error message when dealer submit fails', done => {
  const mockComponentDidMount = Promise.resolve(
    new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
      status: 200,
      headers: { 'content-type': 'application/json' }
    })
  );
  const mockButtonClickFetchError = Promise.reject(new Error('my error'));

  jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
  const element = mount(<DealerSelection />);

  process.nextTick(() => {
    jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
    const button = element.find('button');
    button.simulate('click');
    process.nextTick(() => {
      console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
      global.fetch.mockClear();
      done();
    });
  });
});

Это компонентчто я на самом деле использую:

import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '@omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';

class DealerSelection extends Component {

  state = {
    loading: true,
    error: null,
    dealer: '',
    options: []
  }

  componentDidMount() {
    document.title = "Select dealer";

    fetchBackend(
      '/agent/account/dealerlist',
      {},
      this.onDealerListSuccessHandler,
      this.onFetchErrorHandler
    );
  }

  onDealerListSuccessHandler = json => {
    const options = json.data.map((item) => {
      return {
        key: item.key,
        value: item.key,
        text: item.value
      };
    });
    this.setState({
      loading: false,
      options,
      dealer: json.default
    });
  }

  onFetchErrorHandler = err => {
    if (err instanceof ClientError) {
      err.response.json().then(data => {
        this.setState({
          error: data.error,
          loading: false
        });
      });
    } else {
      console.log('set error to', err.message);
      this.setState({
        error: err.message,
        loading: false
      });
    }
  }

  onSubmitHandler = () => {
    const { dealer } = this.state;
    this.setState({
      loading: true,
      error: null
    });

    fetchBackend(
      '/agent/account/dealerPost',
      {
        dealer
      },
      this.onDealerSelectSuccessHandler,
      this.onFetchErrorHandler
    );
  }

  onDealerSelectSuccessHandler = json => {
    if (!json.error) {
      window.location = json.redirect; // Refresh to return back to MVC
    }
    this.setState({
      error: json.error
    });
  }

  onChangeHandler = (event, key) => {
    this.setState({
      dealer: event.target.value
    });
  }

  render() {
    const { loading, error, dealer, options } = this.state;
    const errorBanner = error ? <Banner type='error' text={error} /> : null;

    return (
      <div className='dealerselection'>
        <Form>
          <Header as="h1">Dealer selection</Header>
          { errorBanner }
          <Select
            label='My dealer'
            fluid
            defaultValue={dealer}
            onChange={this.onChangeHandler}
            maxHeight={5}
            options={options}
          />
          <Button
            primary
            fluid
            onClick={this.onSubmitHandler}
            loading={loading}
          >Select dealer</Button>
        </Form>
      </div>
    );
  }
}

export default DealerSelection;

Ответы [ 2 ]

0 голосов
/ 14 марта 2019

Интересно, что на этот раз потребовалось немного времени.


Соответствующие части из документа Node.js по Цикл событий, таймеры и process.nextTick():

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

... каждый раз, когда вы вызываете process.nextTick() в данной фазе, все обратные вызовы пройденызначение process.nextTick() будет разрешено до продолжения цикла событий.

Другими словами, Node начинает обработку nextTickQueue после завершения текущей операции и будет продолжать до очередипусто перед продолжением цикла обработки событий.

Это означает, что если process.nextTick() вызывается во время обработки nextTickQueue, обратный вызов добавляется в очередь, и он будет обработанперед продолжением цикла событий .

Документ предупреждает:

Это может создать некоторые плохие ситуации, потому что позволяет вам "голодать" ваш ввод / вывод с помощьюсовершая рекурсивные process.nextTick() вызовы , что не позволяет циклу событий достичь фазы poll .

... и, как оказывается, вы можете голодать Promise также обратные вызовы:

test('Promise and process.nextTick order', done => {
  const order = [];

  Promise.resolve().then(() => { order.push('2') });

  process.nextTick(() => {
    Promise.resolve().then(() => { order.push('7') });
    order.push('3');  // this runs while processing the nextTickQueue...
    process.nextTick(() => {
      order.push('4');  // ...so all of these...
      process.nextTick(() => {
        order.push('5');  // ...get processed...
        process.nextTick(() => {
          order.push('6');  // ...before the event loop continues...
        });
      });
    });
  });

  order.push('1');

  setTimeout(() => {
    expect(order).toEqual(['1','2','3','4','5','6','7']);  // ...and 7 gets added last
    done();
  }, 0);
});

Таким образом, в этом случае вложенный process.nextTick() обратный вызов, который регистрирует element.state(), завершается выполнением перед Promise обратными вызовами, которые установят state.error в 'my error'.


Именно по этой причине документ рекомендует следующее:

Мы рекомендуем разработчикам использовать setImmediate() во всех случаях, потому что проще рассуждать о


Если вы измените свои process.nextTick вызовы на setImmediate (и создадите свои fetch макеты как функций , поэтому Promise.reject() не запустится немедленно и не вызоветошибка), тогда ваш тест должен работать как положено:

it('throws error message when dealer submit fails', done => {
  const mockComponentDidMount = () => Promise.resolve(
    new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
      status: 200,
      headers: { 'content-type': 'application/json' }
    })
  );
  const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));

  jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
  const element = mount(<DealerSelection />);

  setImmediate(() => {
    jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
    const button = element.find('button');
    button.simulate('click');
    setImmediate(() => {
      console.log(element.state()); // state.error is 'my error'
      global.fetch.mockClear();
      done();
    });
  });
});
0 голосов
/ 06 марта 2019

Для обновления состояния требуется несколько асинхронных вызовов, поэтому ваш process.nextTick() не достаточен.Чтобы обновить состояние, это должно произойти:

  • ваш тестовый код щелкает, и обратный вызов обработчика событий ставится в очередь
  • обратный вызов обработчика событий запускается, запускается fetch, получаетотклонение обещания и запуск обработчика ошибок
  • обработчик ошибок запускает setState, который ставит в очередь обновление состояния (setState асинхронно!)
  • ваш тестовый код выполняется, проверяя состояние элемента
  • обновление состояния выполняется

Короче говоря, вам нужно подождать дольше, прежде чем устанавливать состояние.

Полезная идиома "ждать" без вложенного process.nextTick() Позволяет определить помощника по тестированию

function wait() {
    return new Promise((resolve) => setTimeout(resolve));
}

, а затем выполнить

await wait();

столько раз, сколько требуется в вашем тестовом коде.Обратите внимание, что для этого необходимо определить функции тестирования как

test(async () => {

})

, а не

test(done => {

})
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...