Redux: Unit-Tests für intelligente Komponenten

Erstellt am 20. Aug. 2015  ·  19Kommentare  ·  Quelle: reduxjs/redux

Ich habe den Abschnitt zum Testen von Einheiten in der Dokumentation gelesen, und obwohl er ein Beispiel für das Testen einer dummen Komponente enthält, wird die Möglichkeit, eine "intelligente Komponente" (diejenigen, die die Methode connect () verwenden) zu testen, nicht behandelt. Es stellt sich heraus, dass das Testen von Einheiten einer intelligenten Komponente aufgrund der von connect () erstellten Wrapper-Komponente etwas komplizierter ist. Ein Teil des Problems besteht darin, dass für das Umschließen einer Komponente über connect () eine 'store'-Requisite (oder ein Kontext) erforderlich ist.

Ich habe versucht, dies zu tun, und ich hatte gehofft, ein kleines Feedback darüber zu bekommen, ob es einen besseren Weg gibt, dies zu erreichen. Und wenn das, was ich getan habe, vernünftig aussieht, habe ich mir vorgenommen, eine PR zu starten, um einige Informationen dazu in die Dokumentation zum Komponententest aufzunehmen.

Zu Beginn habe ich die vorhandene Beispielkomponente im Unit-Test-Abschnitt der Dokumente in connect () eingeschlossen, um Daten von Erstellern von Status- und versandgebundenen Aktionen zu übergeben:

Header.js (intelligente Komponente)
import React, { PropTypes, Component } from 'react';
import TodoTextInput from './TodoTextInput';
import TodoActions from '../actions/TodoActions';
import connect from 'redux-react';

class Header extends Component {
  handleSave(text) {
    if (text.length !== 0) {
      this.props.addTodo(text);
    }
  }

  render() {
    return (
      <header className='header'>
          <h1>{this.props.numberOfTodos + " Todos"}</h1>
          <TodoTextInput newTodo={true}
                         onSave={this.handleSave.bind(this)}
                         placeholder='What needs to be done?' />
      </header>
    );
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(Header);

In der Unit-Test-Datei sieht es ebenfalls ähnlich aus wie im Beispiel.

Header.test.js
import expect from 'expect';
import jsdomReact from '../jsdomReact';
import React from 'react/addons';
import Header from '../../components/Header';
import TodoTextInput from '../../components/TodoTextInput';
const { TestUtils } = React.addons;

/**
 * Mock out the top level Redux store with all the required 
 * methods and have it return the provided state by default.
 * <strong i="13">@param</strong> {Object} state State to populate in store
 * <strong i="14">@return</strong> {Object} Mock store
 */
function createMockStore(state) {
  return {
    subscribe: () => {},
    dispatch: () => {},
    getState: () => {
      return {...state};
    }
  };
}

/**
 * Render the Header component with a mock store populated
 * with the provided state
 * <strong i="15">@param</strong> {Object} storeState State to populate in mock store
 * <strong i="16">@return</strong> {Object} Rendered output from component
 */
function setup(storeState) {
  let renderer = TestUtils.createRenderer();
  renderer.render(<Header store={createMockStore(storeState)} />);
  var output = renderer.getRenderedOutput();
  return output.refs.wrappedInstance();
}

describe('components', () => {
  jsdomReact();

  describe('Header', () => {
    it('should call call addTodo if length of text is greater than 0', () => {
      const output = setup({
        todos: [1, 2, 3]
      });
      var addTodoSpy = expect.spyOn(output.props, 'addTodo');

      let input = output.props.children[1];
      input.props.onSave('');
      expect(addTodoSpy.calls.length).toBe(0);
      input.props.onSave('Use Redux');
      expect(addTodoSpy.calls.length).toBe(1);
    });
  });
});

Ich habe diesen Test ein wenig vereinfacht, um nur die relevanten Teile zu zeigen, aber der Hauptpunkt, bei dem ich mir nicht sicher bin, ist die createMockStore -Methode. Wenn Sie versuchen, die Header-Komponente ohne Requisiten zu rendern, wird von Redux (oder React-Redux) ein Fehler ausgegeben, der besagt, dass die Komponente eine store Requisite oder einen Kontext haben muss, da sie erwartet, ein Kind der zu sein <Provider> Komponente. Da ich das nicht für meine Komponententests verwenden möchte, habe ich eine Methode erstellt, um es zu verspotten und den Test in dem Zustand zu bestehen, den es im Geschäft festlegen möchte.

Der Vorteil dieses Ansatzes besteht darin, dass ich die Funktionen in meiner Komponente testen kann, aber auch die Funktionalität der Methoden testen kann, die ich an connect () übergebe. Ich könnte hier leicht eine weitere Behauptung schreiben, die so etwas wie expect(output.props.numberOfTodos).toBe(3) , die bestätigt, dass meine mapStateToProps -Funktion das tut, was ich erwarte.

Der Hauptnachteil davon ist, dass ich den Redux-Store verspotten muss, der nicht allzu komplex ist, aber es scheint, als sei er Teil der internen Redux-Logik und könnte sich ändern. Offensichtlich habe ich diese Methoden für meine Komponententests in eine allgemeine Unit-Test-Dienstprogrammdatei verschoben. Wenn sich also die Speichermethoden ändern würden, müsste ich meinen Code nur an einer Stelle ändern.

Gedanken? Hat noch jemand mit Unit-Tests für intelligente Komponenten experimentiert und einen besseren Weg gefunden, Dinge zu tun?

discussion question

Hilfreichster Kommentar

@ernieturner Ich denke, was @ghengeveld bedeutet, ist, wenn Sie überall ES6-Module verwenden, die dekorierte Header-Komponente weiterhin als Standard exportiert werden kann und eine einfache React-Komponente ein zusätzlicher benannter Export sein kann. Zum Beispiel -

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

Mit diesem doppelten Export kann Ihre Haupt-App die intelligente Komponente wie zuvor mit import Header from './header.js' verbrauchen, während Ihre Tests Redux umgehen und das Verhalten der Kernkomponente direkt mit import {HeaderDumbComponent} from '../components/header.js' testen können

Alle 19 Kommentare

Die Alternative besteht darin, auch die (nicht dekorierte) Header-Klasse zu exportieren, damit sie separat importiert und getestet werden kann.

Außerdem benötigen Sie den Aufruf jsdomReact nicht in jeder Datei, zumindest wenn Sie Mocha verwenden. Hier ist meine setup.js:

import jsdom from 'jsdom';
import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';

if (!global.document || !global.window) {
  global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
  global.window = document.defaultView;
  global.navigator = window.navigator;

  ExecutionEnvironment.canUseDOM = true;

  window.addEventListener('load', () => {
    console.log('JSDom setup completed: document, window and navigator are now on global scope.');
  });
}

Es wird über die Befehlszeilenoption --require geladen: mocha -r babelhook -r test/setup --recursive (babelhook ist ein Aufruf von require('babel-core/register') ).

Ja, ich konnte sowohl die Header-Klasse als auch die dekorierte exportieren, aber ich hatte gehofft, den Quellcode nicht nur für meine Unit-Tests ändern zu müssen. Wenn ich das mache, müsste ich dann alle Stellen ändern, die eine dekorierte Komponente enthalten (z. B. import {DecoratedHeader} from './components/Header' statt nur import Header from . / Components / Header`).

Was das jsdom-Setup betrifft, habe ich nur das Beispiel aus den Dokumenten kopiert, um den Anwendungsfall zu zeigen. Ich verwende das nicht als mein Haupt-Setup, sondern nur als Beispiel.

Ich habe gerade festgestellt, dass mein Codebeispiel oben unvollständig war. Um eine intelligente Komponente direkt zu testen, muss eine Dienstprogrammmethode Ihnen das refs.wrappedInstance anstatt nur das Ergebnis des Renderns, da Sie dann die verbindungsdekorierte Komponente erhalten (aktualisiert in der obigen Setup-Funktion). . Dies führt wiederum dazu, dass die Komponententests von Redux-Interna abhängen (in diesem Fall speziell von den React-Redux-Interna). So funktioniert es, fühlt sich aber etwas zerbrechlich an.

@ernieturner Wir bieten getWrappedInstance() sodass Sie sich darauf verlassen können, wenn Sie Bedenken haben, direkt auf refs zuzugreifen. Und es gibt auch Dienstprogramme wie den React-Test-Tree, um dies transparent zu machen.

@ernieturner Ich denke, was @ghengeveld bedeutet, ist, wenn Sie überall ES6-Module verwenden, die dekorierte Header-Komponente weiterhin als Standard exportiert werden kann und eine einfache React-Komponente ein zusätzlicher benannter Export sein kann. Zum Beispiel -

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

Mit diesem doppelten Export kann Ihre Haupt-App die intelligente Komponente wie zuvor mit import Header from './header.js' verbrauchen, während Ihre Tests Redux umgehen und das Verhalten der Kernkomponente direkt mit import {HeaderDumbComponent} from '../components/header.js' testen können

@ eugene1g @ghengeveld Dies ist eigentlich eine wirklich gute Lösung für das Problem. Sie können gerne eine PR an das Dokument "Schreibtests" senden, in dem dies erklärt wird!

@ eugene1g Genau das habe ich gemeint.
@gaearon Das mache ich.

@ghengeveld Entschuldigung für das Missverständnis. Ich verwende immer noch Importe im CommonJS-Stil, daher bin ich auf ES6-Modulen immer noch ziemlich verrostet. Das scheint eine gute Lösung zu sein, um sie in den Dokumenten verfügbar zu machen. Ich nehme an, Sie könnten auch die mapStateToProps / mapDispatchToProps-Methoden verfügbar machen, damit diese getestet werden können, ohne sich mit dem Verspotten des Geschäfts befassen zu müssen, z

export class HeaderDumpComponent ...

export function mapStateToProps(state) ... 
export function mapDispatchToProps(dispatch) ...

export default connect(mapStateToProps, mapDispatchToProps)(HeaderDumpComponent);

Es ist durchaus möglich, dass die meisten Kartenmethoden so einfach sind, dass keine Unit-Tests erforderlich sind, aber ich habe einige davon, die es wert wären, überprüft zu werden, da sie einen so wichtigen Teil der Funktionalität meiner Komponenten ausmachen.

Vielen Dank für die Vorschläge an alle, wenn Sie Hilfe bei der Dokumentation benötigen, lassen Sie es mich wissen.

Wir versuchen gerade den oben beschriebenen empfohlenen Ansatz, fühlen uns aber damit nicht ganz wohl. Dadurch bleiben die an connect Parameter außerhalb von Integrationstests ungetestet. Ich verstehe, dass ein Mock-Store möglicherweise spröde sein könnte, aber ich denke, hier könnte so etwas wie ein Redux-Mock-Store einspringen, wenn er Komponententests unterstützen könnte. Wie denkst du über diese Richtung?

@ carpeliam

Warum nicht einfach einen normalen Laden benutzen und ihn mit einem bestimmten Ausgangszustand hydratisieren?
Sie müssen den Speicher nur verspotten, wenn Sie testen möchten, welche Aktionen ausgelöst werden.

@ carpeliam

In den Unit-Tests finden Sie Beispiele für counter und todomvc in diesem Repo.

Wir hatten ein Problem, bei dem wir eine intelligente Komponente innerhalb einer anderen Komponente gerendert haben und den flachen Renderer nicht verwenden konnten (Komponente verwendet componentDid * -Methoden). Was wir getan haben, war Stub (mit sinon) die Verbindungsfunktion, um eine Funktion zurückzugeben, die eine einfache React-Komponente zurückgibt.

Es ist spröde und wir hoffen, dass wir zum flachen Renderer wechseln können, sobald wir zu React 0.14 migrieren können, aber diese Methode hat uns bei unseren Tests vorerst entsperrt.

Ich habe auch Probleme damit. Ich bin sowohl für React als auch für Redux ziemlich neu, daher ist es wahrscheinlich, was mich zurückhält. Wie konnten Sie das umgehen? @ongawee

Die vorgeschlagene Doppelexportmethode lässt die Funktion select (mapStoreToState) ungetestet. Um es unabhängig zu testen, muss es ebenfalls exportiert werden, eine weitere Änderung im Namen des Tests.

Ich wäre daran interessiert, einen Weg zu finden, wie flatRenderer mit der umhüllten Komponente von connect arbeiten kann. Mein aktuelles Problem ist, dass bei Verwendung eines flachen Renderers nur die Connect-Komponente zurückgegeben wird, was zu erwarten ist.

Die vorgeschlagene Doppelexportmethode lässt die Funktion select (mapStoreToState) ungetestet. Um es unabhängig zu testen, muss es ebenfalls exportiert werden, eine weitere Änderung im Namen des Tests.

Nicht ausschließlich "im Namen der Prüfung". Tatsächlich empfehlen wir Ihnen, neben Ihren Reduzierern Selektorfunktionen zu definieren (was letztendlich mapStateToProps ist) und diese gemeinsam zu testen. Diese Logik ist unabhängig von der Benutzeroberfläche und muss nicht damit gebündelt werden. Schauen Sie sich das Beispiel shopping-cart für einige Selektoren an.

Also @gaearon ,

Ja, das allgemein vorgeschlagene Muster für Redux besteht darin, Auswahlfunktionen praktisch überall zu verwenden, anstatt direkt auf state.some.nested.field zuzugreifen. Sie können sehr einfache "einfache" Funktionen sein, werden jedoch am häufigsten mithilfe der Reselect-Bibliothek zusammengestellt, die Memo-Funktionen bietet.

Warum würde das zusätzlichen Aufwand verursachen?

Ich habe den Doppelexport wie hier beschrieben durchgeführt, aber beim Lesen der Quelle connect mir klar, dass der "Container" einen Verweis auf die "dumme" Komponente "in der statischen Eigenschaft WrappedComponent :

Also statt:

// header.js
export const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import { HeaderDumbComponent } from './header'

it('renders', () => {
  expect(<HeaderDumbComponent />).to.not.be.null
})

Sie können den doppelten Export vermeiden, indem Sie WrappedComponent wo Sie die "dumme" Version benötigen:

// header.js
const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import Header from './header'

it('renders', () => {
  expect(<Header.WrappedComponent />).to.not.be.null
})

@gaearon - WrappedComponent scheint eine ziemlich stabile Eigenschaft zu sein, da sie in den Dokumenten enthalten ist. Würden Sie aus irgendeinem Grund davon abraten, es auf diese Weise zu verwenden?

Ich exportiere die nicht verpackte Komponente und exportiere auch mapStateToProps und mapDispatchToProps, damit ich diese Funktionen einem Unit-Test unterziehen kann. Hier ist ein Beispiel:

import {ManageCoursePage, mapStateToProps, mapDispatchToProps} from './ManageCoursePage';
describe("mapStateToProps", () => {
    function setup(courses, authors, id) {
        const state = {
            authors: authors
        };

        const ownProps = {
            params: {
                id: id
            }
        };

        return mapStateToProps(state, ownProps);
    }

    it('sets the course when a course id is set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "1";

        const props = setup(courses, authors, id);

        expect(props.course).toBe(courses[0]);
    });

    it('sets the course when a course id is not set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the course when a course id is set that is not present in the courses list', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "42";

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the authors formatted for a drop down list', () => {
        const courses = [];
        const authors = [ { id: "1", name: "John" }, { id: "2", name: "Jill" }];

        const props = setup(courses, authors, null);

        expect(props.authors).toEqual([
            { value: "1", text: "John" },
            { value: "2", text: "Jill" }
        ])
    });
});
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen