Ферментное тестирование компонента высшего порядка аутентификации (HOC)

Я создал компонент высшего порядка / составной компонент, чтобы убедиться, что пользователь аутентифицирован перед загрузкой компонента. Это очень просто, но у меня возникли проблемы с его тестированием. Я хочу проверить пункты ниже, которые аналогичны тестам, которые у меня уже есть в другом месте:

  • Визуализирует компонент (обычно я проверяю, ища конкретный компонент className)
  • Имеет правильный props (в моем случае authenticated)
  • Отрисовывает обернутый компонент, если authenticated, и null, если нет

ОС:

import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';

import { makeSelectAuthenticated } from 'containers/App/selectors';

export default function RequireAuth(ComposedComponent) {
  class AuthenticatedComponent extends React.Component {
    static contextTypes = {
      router: React.PropTypes.object,
    }

    static propTypes = {
      authenticated: React.PropTypes.bool,
    }

    componentWillMount() {
      if (!this.props.authenticated) this.context.router.push('/');
    }

    componentWillUpdate(nextProps) {
      if (!nextProps.authenticated) this.context.router.push('/');
    }

    render() {
      return (
        <div className="authenticated">
          { this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
        </div>
      );
    }
  }

  const mapStateToProps = createStructuredSelector({
    authenticated: makeSelectAuthenticated(),
  });

  return connect(mapStateToProps)(AuthenticatedComponent);
}

Я использую enzyme и jest для своих тестов, но не нашел способа успешно отобразить HOC во время своих тестов.

Любые идеи?

Решение благодаря ответу ниже:

import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';

import { AuthenticatedComponent } from '../index';

describe('AuthenticatedComponent', () => {
  let MockComponent;

  beforeEach(() => {
    MockComponent = () => <div />;
    MockComponent.displayName = 'MockComponent';
  });

  it('renders its children when authenticated', () => {
    const wrapper = shallow(
      <AuthenticatedComponent
        composedComponent={MockComponent}
        authenticated={true}
      />,
      { context: { router: { push: jest.fn() } } }
    );

    expect(wrapper.find('MockComponent').length).toEqual(1);
  });

  it('renders null when not authenticated', () => {
    const wrapper = shallow(
      <AuthenticatedComponent
        composedComponent={MockComponent}
        authenticated={false}
      />,
      { context: { router: { push: jest.fn() } } }
    );

    expect(wrapper.find('MockComponent').length).toEqual(0);
  });
});

person germainelol    schedule 19.01.2017    source источник


Ответы (1)


«Сложная» часть здесь заключается в том, что ваш HOC возвращает подключенный компонент, что усложняет тестирование, потому что у вас есть неглубокий рендеринг двух слоев (подключенный компонент и фактический компонент), и вам нужно имитировать хранилище избыточности.

Вместо этого вы можете определить AuthenticatedComponent заранее и экспортировать его как именованный экспорт. Затем вы можете протестировать его независимо от connect, как вы тестируете любой другой компонент:

export class AuthenticatedComponent extends React.Component {
  static contextTypes = {
    router: React.PropTypes.object,
  }

  static propTypes = {
    authenticated: React.PropTypes.bool,
    composedComponent: React.PropTypes.any.isRequired,
  }

  componentWillMount() {
    if (!this.props.authenticated) this.context.router.push('/');
  }

  componentWillUpdate(nextProps) {
    if (!nextProps.authenticated) this.context.router.push('/');
  }

  render() {
    const ComposedComponent = this.props.composedComponent;
    return (
      <div className="authenticated">
        { this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
      </div>
    );
  }
}

export default function RequireAuth(ComposedComponent) {
  const mapStateToProps = () => {
    const selectIsAuthenticated = makeSelectAuthenticated();
    return (state) => ({
      authenticated: selectIsAuthenticated(state),
      composedComponent: ComposedComponent,
    });
  };

  return connect(mapStateToProps)(AuthenticatedComponent);
}

Пример теста:

import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import RequireAuth, { AuthenticatedComponent } from '../';

const Component = () => <div />;
Component.displayName = 'CustomComponent';

const mockStore = configureStore([]);

describe.only('HOC', () => {
  const RequireAuthComponent = RequireAuth(Component);
  const context = { router: { push: jest.fn() } };
  const wrapper = mount(
    <Provider store={mockStore({})}>
      <RequireAuthComponent />
    </Provider>,
    {
      context,
      childContextTypes: { router: React.PropTypes.object.isRequired },
    }
  );
  it('should return a component', () => {
    expect(wrapper.find('Connect(AuthenticatedComponent)')).toHaveLength(1);
  });
  it('should pass correct props', () => {
    expect(wrapper.find('AuthenticatedComponent').props()).toEqual(
      expect.objectContaining({
        authenticated: false,
        composedComponent: Component,
      })
    );
  });
});

describe('rendering', () => {
  describe('is authenticated', () => {
    const wrapper = shallow(
      <AuthenticatedComponent
        composedComponent={Component}
        authenticated
      />,
      { context: { router: { push: jest.fn() } } }
    );
    it('should render the passed component', () => {
      expect(wrapper.find('CustomComponent')).toHaveLength(1);
    });
  });
  describe('is not authenticated', () => {
    const wrapper = shallow(
      <AuthenticatedComponent
        composedComponent={Component}
        authenticated={false}
      />,
      { context: { router: { push: jest.fn() } } }
    );
    it('should not render the passed component', () => {
      expect(wrapper.find('CustomComponent')).toHaveLength(0);
    });
  });
});
person PhilippSpo    schedule 19.01.2017
comment
Спасибо за пояснение, а нельзя ли привести пример, как просто рендерить компонент в тесте? Я пробовал множество различных методов, используя shallow и mount, но, похоже, ничего не получается правильно отобразить для тестирования. - person germainelol; 19.01.2017
comment
Кроме того, немного смущает переменная composedComponent, которую вы передаете в mapStateToProps. Это по-прежнему будет действовать как обычно? - person germainelol; 19.01.2017
comment
Добавлен пример того, как вы можете протестировать файл AuthenticatedComponent. Я согласен, что использование mapStateToProps для передачи composedComponent выглядит немного странно. Поскольку AuthenticatedComponent теперь определяется вне HOC, единственный способ передать его ComposedComponent — через реквизит. Я подумал, что мы можем просто использовать mapStateToProps или это. - person PhilippSpo; 19.01.2017
comment
Отлично, я опубликую свой окончательный тестовый файл в ОП, чтобы вы могли его проверить, было бы хорошо получить ваши отзывы. Отметил это как правильный ответ - person germainelol; 19.01.2017
comment
Я обнаружил одну проблему с вашим ответом: при попытке загрузить мой обернутый компонент я использую export default RequireAuth(SomeComponent); ... но получаю The prop composedComponent is marked as required in AuthenticatedComponent, but its value is undefined. Очень странно, потому что на самом деле тесты, которые я написал выше, проходят нормально. - person germainelol; 19.01.2017
comment
О, извините, не заметил, что вы используете повторный выбор! Обновлю ответ. - person PhilippSpo; 19.01.2017
comment
Все еще получаю ошибку с реквизитом, но буду работать над этим. HOC › encountered a declaration exception, TypeError: state.get is not a function - это ошибка, которую я предполагаю из-за тестов. - person germainelol; 19.01.2017
comment
Я попробовал это, прежде чем опубликовать, и у меня не возникает никаких ошибок ни во время выполнения в приложении, ни в тестах. Надеюсь, вы разберетесь, что происходит :-) - person PhilippSpo; 19.01.2017
comment
Кажется, это исходит от моего глобального селектора state: const selectGlobal = (state) => state.get('global');.... На самом деле понятия не имею :/ - person germainelol; 19.01.2017
comment
Поскольку в приведенном выше примере больше не используется createStructuredSelector, вам нужно передать состояние вручную, как я делаю для selectIsAuthenticated. Вы делаете это? - person PhilippSpo; 19.01.2017
comment
Не думаю, но я не уверен, где/что я бы изменил, поскольку я также новичок в использовании reselect. - person germainelol; 19.01.2017
comment
Я полагаю, что это может быть из-за того, что я использую immutable для состояния, но понятия не имею, как это исправить - может быть mockStore в наборе тестов. - person germainelol; 19.01.2017
comment
Кажется, сейчас проходят тесты, единственная проблема заключается в том, что когда я регистрирую this.props.authenticated в компоненте, он регистрируется три раза. В первый раз он регистрируется как function, тогда как ожидается, что он будет boolean - person germainelol; 19.01.2017