Redux: Unit menguji komponen pintar

Dibuat pada 20 Agu 2015  ·  19Komentar  ·  Sumber: reduxjs/redux

Saya membaca bagian pengujian unit dari dokumentasi dan sementara itu memiliki contoh cara menguji komponen bodoh, mampu menguji unit "komponen pintar" (yang menggunakan metode connect ()) tidak tercakup. Ternyata pengujian unit komponen cerdas sedikit lebih rumit karena komponen pembungkus yang dibuat oleh connect (). Sebagian dari masalahnya adalah bahwa membungkus komponen melalui connect () membutuhkan prop 'store' (atau konteks).

Saya mengambil celah saat mencoba melakukan ini dan saya berharap mendapatkan sedikit umpan balik tentang apakah ada cara yang lebih baik untuk mencapainya. Dan jika apa yang saya lakukan akhirnya terlihat masuk akal, saya pikir saya akan mendorong PR untuk menambahkan beberapa info tentang ini ke dalam dokumentasi pengujian unit.

Untuk memulai, saya mengambil komponen contoh yang ada di bagian pengujian unit dokumen, dan membungkusnya dengan connect () untuk meneruskan data dari pembuat tindakan yang terikat pada status dan pengiriman:

Header.js (komponen pintar)
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);

Dalam file pengujian unit, tampilannya juga mirip dengan contoh.

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);
    });
  });
});

Saya telah menyederhanakan tes ini sedikit untuk hanya menampilkan bagian yang relevan, tetapi poin utama yang saya tidak yakin adalah metode createMockStore . Jika Anda mencoba dan merender komponen Header tanpa props, kesalahan dilemparkan oleh Redux (atau react-redux) yang mengatakan bahwa komponen harus memiliki store prop atau konteks karena diharapkan menjadi anak dari <Provider> komponen. Karena saya tidak ingin menggunakannya untuk pengujian unit saya, saya membuat metode untuk memalsukannya dan membiarkan pengujian lulus dalam keadaan yang diinginkan di toko.

Manfaat yang dapat saya lihat dari pendekatan ini adalah memungkinkan saya menguji fungsi di dalam komponen saya, tetapi juga dapat menguji fungsionalitas metode yang saya lewati ke connect (). Saya dapat dengan mudah menulis pernyataan lain di sini yang melakukan sesuatu seperti expect(output.props.numberOfTodos).toBe(3) yang memverifikasi bahwa fungsi mapStateToProps saya melakukan apa yang saya harapkan.

Kelemahan utama dari itu adalah bahwa saya harus mengejek toko Redux, yang tidak terlalu rumit, tetapi rasanya itu adalah bagian dari logika Redux internal dan mungkin berubah. Jelas untuk pengujian unit saya, saya telah memindahkan metode ini ke file utilitas pengujian unit umum jadi jika metode penyimpanan memang berubah, saya hanya perlu memodifikasi kode saya di satu tempat.

Pikiran? Adakah orang lain yang bereksperimen dengan komponen pintar pengujian unit dan menemukan cara yang lebih baik untuk melakukan sesuatu?

discussion question

Komentar yang paling membantu

@ernieturner Menurut saya yang dimaksud @ghengeveld adalah jika Anda menggunakan modul ES6 di mana-mana, komponen Header yang dihias masih dapat diekspor sebagai default, dan komponen React biasa dapat menjadi ekspor bernama tambahan. Sebagai contoh -

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

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

Dengan ekspor ganda ini, aplikasi utama Anda dapat menggunakan komponen pintar seperti sebelumnya dengan import Header from './header.js' sementara pengujian Anda dapat melewati redux dan menguji perilaku komponen inti secara langsung dengan import {HeaderDumbComponent} from '../components/header.js'

Semua 19 komentar

Alternatifnya adalah dengan mengekspor kelas Header (tanpa dekorasi), sehingga dapat diimpor dan diuji secara terpisah.

Anda juga tidak memerlukan panggilan jsdomReact di setiap file, setidaknya jika Anda menggunakan Mocha. Inilah setup.js saya:

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.');
  });
}

Ini dimuat melalui opsi baris perintah --require: mocha -r babelhook -r test/setup --recursive (babelhook adalah panggilan ke require('babel-core/register') ).

Ya, saya dapat mengekspor kelas Header dan kelas yang didekorasi, tetapi saya berharap untuk menghindari keharusan mengubah kode sumber hanya untuk pengujian unit saya. Melakukan hal itu akan menyebabkan saya harus mengubah semua tempat yang menyertakan komponen yang didekorasi (misalnya import {DecoratedHeader} from './components/Header' alih-alih hanya import Header from . / Components / Header`).

Adapun pengaturan jsdom, saya hanya menyalin contoh dari dokumen sebagai cara untuk menunjukkan kasus penggunaan, saya tidak menggunakannya sebagai pengaturan utama saya, hanya sebagai contoh.

Saya baru saja memperhatikan bahwa contoh kode saya di atas tidak lengkap. Untuk menguji komponen pintar secara langsung, Anda harus memiliki beberapa metode utilitas yang mengembalikan Anda refs.wrappedInstance alih-alih hanya hasil rendering karena itu akan memberi Anda komponen yang dihias dengan koneksi (diperbarui dalam fungsi pengaturan di atas) . Ini sekali lagi membuat pengujian unit bergantung pada internal Redux (dalam hal ini, khususnya internal react-redux). Jadi berhasil, tetapi terasa agak rapuh.

@ernieturner Kami juga menawarkan getWrappedInstance() API publik untuk ini sehingga Anda dapat mengandalkannya jika Anda khawatir tentang mengakses refs secara langsung. Dan ada utilitas seperti react-test-tree untuk membuat ini transparan juga.

@ernieturner Menurut saya yang dimaksud @ghengeveld adalah jika Anda menggunakan modul ES6 di mana-mana, komponen Header yang dihias masih dapat diekspor sebagai default, dan komponen React biasa dapat menjadi ekspor bernama tambahan. Sebagai contoh -

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

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

Dengan ekspor ganda ini, aplikasi utama Anda dapat menggunakan komponen pintar seperti sebelumnya dengan import Header from './header.js' sementara pengujian Anda dapat melewati redux dan menguji perilaku komponen inti secara langsung dengan import {HeaderDumbComponent} from '../components/header.js'

@ eugene1g @ghengeveld Ini sebenarnya adalah solusi yang sangat bagus untuk masalah ini. Jangan ragu untuk mengirim PR ke dokumen "Tes menulis" yang menjelaskannya!

@ eugene1g Itulah yang saya maksud.
@ Gaearon saya akan melakukan itu.

@ghengeveld Maaf atas kesalahpahaman. Saya masih menggunakan impor gaya CommonJS jadi saya masih cukup berkarat pada modul ES6. Tampaknya itu solusi yang bagus untuk diekspos di dokumen. Saya kira Anda juga bisa mengekspos metode mapStateToProps / mapDispatchToProps juga untuk memungkinkan mereka diuji tanpa harus berurusan dengan mengejek toko, misalnya

export class HeaderDumpComponent ...

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

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

Sangat mungkin sebagian besar metode peta akan cukup sederhana untuk tidak memerlukan pengujian unit, tetapi saya memiliki beberapa di antaranya yang layak diverifikasi karena mereka adalah bagian penting dari fungsionalitas komponen saya.

Terima kasih atas saran semuanya, jika Anda memerlukan bantuan dengan dokumentasi, beri tahu saya.

Kami mencoba pendekatan yang disarankan seperti yang diuraikan di atas sekarang, tetapi kami tidak merasa nyaman sepenuhnya dengannya. Ini membuat parameter yang diteruskan ke connect belum teruji di luar pengujian integrasi. Saya memahami bahwa toko tiruan dapat berpotensi rapuh, tetapi saya merasa di sinilah toko tiruan dapat digunakan, jika dapat mendukung pengujian Komponen. Bagaimana perasaan Anda tentang arah itu?

@tokopedia

Mengapa tidak menggunakan penyimpanan biasa dan melembabkannya dengan keadaan awal tertentu?
Anda hanya perlu toko tiruan jika Anda ingin menguji tindakan mana yang sedang dikirim.

@tokopedia

Silakan lihat pengujian unit untuk counter dan todomvc contoh di repo ini.

Kami mengalami masalah saat kami merender komponen pintar di dalam komponen lain dan tidak dapat menggunakan perender dangkal (komponen menggunakan metode componentDid *). Apa yang kami lakukan adalah stub (menggunakan sinon) fungsi connect untuk mengembalikan fungsi yang mengembalikan komponen React sederhana.

Ini rapuh dan kami berharap untuk pindah ke perender dangkal setelah kami dapat bermigrasi ke React 0.14, tetapi metode ini membuka blokir kami pada pengujian kami untuk saat ini.

Saya juga mengalami masalah dengan ini. Saya agak baru dalam React dan Redux, jadi ini mungkin yang menahan saya. Bagaimana Anda bisa menyiasatinya? @gay_jogja

Metode ekspor ganda yang diusulkan membiarkan fungsi select (mapStoreToState) tidak teruji. Untuk mengujinya secara independen, itu perlu diekspor juga, namun ada perubahan lain dalam nama pengujian.

Saya tertarik untuk menemukan cara agar shallowRenderer bekerja dengan komponen terbungkus connect. Masalah saya saat ini adalah ketika menggunakan perender dangkal itu hanya meneruskan kembali komponen Connect, yang diharapkan.

Metode ekspor ganda yang diusulkan membiarkan fungsi select (mapStoreToState) tidak teruji. Untuk mengujinya secara independen, itu perlu diekspor juga, namun ada perubahan lain dalam nama pengujian.

Tidak hanya "atas nama pengujian". Faktanya, kami mendorong Anda untuk menentukan fungsi pemilih (yang pada akhirnya adalah mapStateToProps ) di samping reduksi Anda, dan mengujinya bersama. Logika ini tidak bergantung pada UI dan tidak harus digabungkan dengannya. Lihat contoh shopping-cart untuk beberapa penyeleksi.

Jadi @gaearon , apakah Anda menyarankan untuk mendapatkan semua data dari status menggunakan fungsi _selector_ ini? Bukankah itu menimbulkan banyak overhead yang tidak diperlukan, karena dalam kebanyakan kasus orang hanya akan membaca sekumpulan properti dari negara bagian dan menetapkannya ke properti pada komponen?

Ya, pola umum yang disarankan untuk Redux adalah menggunakan fungsi selektor di mana-mana, daripada mengakses state.some.nested.field secara langsung. Mereka bisa menjadi fungsi "biasa" yang sangat sederhana, tetapi paling sering disatukan menggunakan pustaka Pilih Ulang, yang menyediakan kemampuan memoization.

Mengapa hal itu menimbulkan biaya tambahan?

Saya telah melakukan ekspor ganda seperti yang dijelaskan di sini tetapi membaca connect source saya menyadari "Container" menyimpan referensi ke "dumb" component "dalam properti WrappedComponent static:

Jadi, alih-alih:

// 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
})

Anda dapat menghindari ekspor ganda dengan menggunakan WrappedComponent mana Anda memerlukan versi "bodoh":

// 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 tampaknya merupakan properti yang cukup stabil mengingat ini ada di dokumen. Apakah Anda akan menyarankan agar tidak menggunakannya dengan cara ini untuk alasan apa pun?

Apa yang saya lakukan adalah mengekspor komponen yang tidak dibungkus, dan juga mengekspor mapStateToProps dan mapDispatchToProps sehingga saya dapat menguji unit fungsi tersebut. Berikut ini contohnya:

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" }
        ])
    });
});
Apakah halaman ini membantu?
0 / 5 - 0 peringkat