Redux: وحدة اختبار المكونات الذكية

تم إنشاؤها على ٢٠ أغسطس ٢٠١٥  ·  19تعليقات  ·  مصدر: reduxjs/redux

كنت أقرأ من خلال قسم اختبار الوحدة في الوثائق وعلى الرغم من أنه يحتوي على مثال لكيفية اختبار مكون غبي ، فإن القدرة على اختبار الوحدة "المكون الذكي" (تلك التي تستخدم طريقة الاتصال ()) غير مشمولة. اتضح أن اختبار الوحدة لمكون ذكي أكثر تعقيدًا بعض الشيء بسبب مكون الغلاف الذي يقوم بتوصيل (). جزء من المشكلة هو أن التفاف المكون عبر connect () يتطلب وجود خاصية "store" (أو سياق).

لقد اتخذت شرعًا في محاولة القيام بذلك وكنت آمل في الحصول على القليل من التعليقات حول ما إذا كانت هناك طريقة أفضل لتحقيق ذلك. وإذا كان ما فعلته يبدو معقولًا ، فقد أدركت أنني سأدفع العلاقات العامة لإضافة بعض المعلومات حول هذا في وثائق اختبار الوحدة.

للبدء ، أخذت مكون المثال الحالي في قسم اختبار الوحدة بالمستندات ، ولفته في connect () لتمرير البيانات من منشئي الإجراءات المرتبطة بالحالة والإرسال:

Header.js (مكون ذكي)
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);

في ملف اختبار الوحدة يبدو مشابهًا للمثال أيضًا.

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

لقد قمت بتبسيط هذا الاختبار قليلاً لإظهار الأجزاء ذات الصلة فقط ، لكن النقطة الرئيسية التي لست متأكدًا منها هي طريقة createMockStore . إذا حاولت عرض مكون Header بدون أي خاصيات ، فسيتم طرح خطأ بواسطة Redux (أو رد فعل-redux) يشير إلى أن المكون يجب أن يكون له خاصية أو سياق store لأنه يتوقع أن يكون تابعًا لـ مكون <Provider> . نظرًا لأنني لا أرغب في استخدام ذلك في اختبارات الوحدة الخاصة بي ، فقد قمت بإنشاء طريقة للاستهزاء بها والسماح للاختبار بالمرور في الحالة التي يريدها في المتجر.

الفائدة التي يمكنني رؤيتها من هذا الأسلوب هي أنه يسمح لي باختبار الوظائف داخل المكون الخاص بي ، ولكن أيضًا سأكون قادرًا على اختبار وظائف الطرق التي أقوم بتمريرها إلى الاتصال (). يمكنني بسهولة كتابة تأكيد آخر هنا يقوم بشيء مثل expect(output.props.numberOfTodos).toBe(3) والذي يتحقق من أن وظيفتي mapStateToProps تقوم بما أتوقعه.

الجانب السلبي الرئيسي لذلك هو أنني مضطر إلى الاستهزاء من متجر Redux ، وهو ليس بكل هذا التعقيد ، لكنه يبدو أنه جزء من منطق Redux الداخلي وقد يتغير. من الواضح أنه بالنسبة لاختبارات الوحدة الخاصة بي ، قمت بنقل هذه الطرق إلى ملف أداة عامة لاختبار الوحدة ، لذا إذا تغيرت طرق المتجر ، فسيتعين علي تعديل الكود الخاص بي في مكان واحد فقط.

أفكار؟ هل جرب أي شخص آخر وحدة اختبار المكونات الذكية ووجد طريقة أفضل للقيام بالأشياء؟

discussion question

التعليق الأكثر فائدة

ernieturner أعتقد أن ما تعنيه ghengeveld هو إذا كنت تستخدم وحدات ES6 في كل مكان ، فلا يزال من الممكن تصدير مكون الرأس المزين باعتباره العنصر الافتراضي ، ويمكن أن يكون مكون React العادي عبارة عن تصدير مسمى إضافي. فمثلا -

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

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

باستخدام هذا التصدير المزدوج ، يمكن لتطبيقك الرئيسي أن يستهلك المكون الذكي كما كان من قبل بـ import Header from './header.js' بينما يمكن لاختباراتك تجاوز الإعادة واختبار سلوك المكون الأساسي مباشرةً باستخدام import {HeaderDumbComponent} from '../components/header.js'

ال 19 كومينتر

البديل هو أيضًا تصدير فئة الرأس (غير المزخرفة) ، بحيث يمكن استيرادها واختبارها بشكل منفصل.

كما أنك لست بحاجة إلى استدعاء jsdomReact في كل ملف ، على الأقل إذا كنت تستخدم Mocha. هذا هو 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.');
  });
}

يتم تحميله عبر خيار سطر الأوامر --require: mocha -r babelhook -r test/setup --recursive (babelhook عبارة عن مكالمة إلى require('babel-core/register') ).

يا ، يمكنني تصدير كل من فئة Header والفئة المزخرفة ، لكنني كنت أتمنى تجنب الاضطرار إلى تغيير كود المصدر فقط لاختبارات الوحدة الخاصة بي. سيؤدي القيام بذلك إلى اضطراري إلى تغيير جميع الأماكن التي تتضمن مكونًا مزخرفًا (على سبيل المثال import {DecoratedHeader} from './components/Header' بدلاً من import Header from . / component / Header`).

بالنسبة إلى إعداد jsdom ، كنت أقوم فقط بنسخ المثال من المستندات كطريقة لإظهار حالة الاستخدام ، ولا أستخدم ذلك كإعداد رئيسي ، بل مجرد مثال.

لقد لاحظت للتو أن نموذج الشفرة أعلاه لم يكن مكتملًا. من أجل اختبار مكون ذكي بشكل مباشر ، يجب أن يكون لديك طريقة مساعدة تعيد لك refs.wrappedInstance بدلاً من مجرد نتيجة العرض لأن ذلك سيمنحك المكون المزين بالاتصال (تم تحديثه في وظيفة الإعداد أعلاه) . هذا النوع مرة أخرى يجعل اختبارات الوحدة تعتمد على العناصر الداخلية للإحياء (في هذه الحالة ، على وجه التحديد ، الأجزاء الداخلية للتفاعل والإعادة). لذلك فهو يعمل ، لكنه يشعر بأنه هش قليلاً.

ernieturner نوفر أيضًا واجهة برمجة تطبيقات عامة getWrappedInstance() لذلك يمكنك الاعتماد عليها إذا كنت قلقًا بشأن الوصول إلى refs مباشرةً. وهناك أدوات مساعدة مثل شجرة اختبار التفاعل لجعل هذه شفافة أيضًا.

ernieturner أعتقد أن ما تعنيه ghengeveld هو إذا كنت تستخدم وحدات ES6 في كل مكان ، فلا يزال من الممكن تصدير مكون الرأس المزين باعتباره العنصر الافتراضي ، ويمكن أن يكون مكون React العادي عبارة عن تصدير مسمى إضافي. فمثلا -

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

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

باستخدام هذا التصدير المزدوج ، يمكن لتطبيقك الرئيسي أن يستهلك المكون الذكي كما كان من قبل بـ import Header from './header.js' بينما يمكن لاختباراتك تجاوز الإعادة واختبار سلوك المكون الأساسي مباشرةً باستخدام import {HeaderDumbComponent} from '../components/header.js'

@ eugene1gghengeveld هذا هو في الواقع حل جيد حقا لهذه المشكلة. لا تتردد في إرسال العلاقات العامة إلى مستند "اختبارات الكتابة" لشرحها!

@ eugene1g هذا بالضبط ما قصدته.
gaearon سأفعل ذلك.

ghengeveld اعتذار عن سوء التفاهم. ما زلت أستخدم واردات نمط CommonJS لذا فأنا لا أزال صدئًا على وحدات ES6. يبدو أن هذا حلاً جيدًا للفضح في المستندات. أفترض أنه يمكنك أيضًا كشف طرق mapStateToProps / mapDispatchToProps أيضًا للسماح باختبارها دون الحاجة إلى التعامل مع الاستهزاء بالمتجر ، على سبيل المثال

export class HeaderDumpComponent ...

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

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

من المحتمل جدًا أن تكون معظم طرق الخرائط بسيطة بما يكفي لعدم استلزام اختبار الوحدة ، لكن لدي القليل منها يستحق التحقق نظرًا لأنها جزء مهم من وظائف مكوناتي.

شكرًا للجميع على الاقتراحات ، إذا كنت بحاجة إلى أي مساعدة في الوثائق ، فأعلمني بذلك.

نحن نحاول النهج الموصى به كما هو موضح أعلاه في الوقت الحالي ، لكننا لا نشعر بالراحة تمامًا تجاهه. هذا يترك المعلمات التي تم تمريرها إلى connect غير مختبرة خارج اختبارات التكامل. أفهم أن متجرًا وهميًا قد يكون هشًا ، لكنني أشعر أن هذا هو المكان الذي يمكن أن يقفز فيه شيء مثل

تضمين التغريدة

لماذا لا تستخدم فقط متجرًا عاديًا وترطيبه بحالة أولية معينة؟
ما عليك سوى محاكاة المتجر إذا كنت تريد اختبار الإجراءات التي يتم إرسالها.

تضمين التغريدة

يرجى الاطلاع على اختبارات الوحدة لـ counter و todomvc أمثلة في هذا الريبو

كنا نواجه مشكلة حيث كنا نقدم مكونًا ذكيًا داخل مكون آخر ولا يمكننا استخدام العارض الضحل (استخدم المكون طرق componentDid *). ما فعلناه كان stub (باستخدام sinon) وظيفة الاتصال لإرجاع دالة تعيد مكون React بسيطًا.

إنه هش ونأمل أن ننتقل إلى العارض الضحل بمجرد أن نتمكن من الانتقال إلى React 0.14 ، لكن هذه الطريقة حررتنا في اختباراتنا في الوقت الحالي.

أواجه بعض المشاكل مع هذا أيضًا. أنا جديد نوعًا ما على كل من React و Redux على حد سواء ، لذلك من المحتمل أن هذا هو ما يعيقني. كيف تمكنت من الالتفاف حوله؟ تضمين التغريدة

تترك طريقة التصدير المزدوجة المقترحة وظيفة التحديد (mapStoreToState) غير مختبرة. من أجل اختباره بشكل مستقل ، يجب تصديره أيضًا ، وهو تغيير آخر في اسم الاختبار.

سأكون مهتمًا بإيجاد طريقة للحصول على عرض سطحي للعمل مع المكون الملفوف الخاص بـ connect. مشكلتي الحالية هي أنه عند استخدام العارض الضحل ، فإنه يقوم فقط بإعادة مكون الاتصال ، وهو أمر متوقع.

تترك طريقة التصدير المزدوجة المقترحة وظيفة التحديد (mapStoreToState) غير مختبرة. من أجل اختباره بشكل مستقل ، يجب تصديره أيضًا ، وهو تغيير آخر في اسم الاختبار.

ليس بصرامة "باسم الاختبار". في الواقع ، نشجعك على تحديد وظائف المحدد (وهو ما يمثله mapStateToProps النهاية) جنبًا إلى جنب مع أدوات التخفيض ، واختبارها معًا. هذا المنطق مستقل عن واجهة المستخدم ولا يجب أن يتم دمجه معه. ألق نظرة على مثال shopping-cart لبعض المحددات.

إذن gaearon ، هل تقترح الحصول على جميع البيانات من الولاية باستخدام وظائف _selector_ هذه؟ ألا ينتج عن ذلك الكثير من النفقات غير الضرورية ، لأنه في معظم الحالات سيقرأ الناس فقط مجموعة من الخصائص من الحالة ويعينونها للدعائم على المكونات؟

نعم ، النمط العام المقترح لـ Redux هو استخدام وظائف المحدد إلى حد كبير في كل مكان ، بدلاً من الوصول إلى state.some.nested.field مباشرة. يمكن أن تكون وظائف "بسيطة" جدًا ، ولكن يتم تجميعها بشكل شائع باستخدام مكتبة إعادة التحديد ، التي توفر إمكانات الحفظ.

لماذا يؤدي ذلك إلى إنشاء أي نفقات إضافية؟

لقد كنت أقوم بالتصدير المزدوج كما هو موضح هنا ولكني قرأت المصدر connect أدركت أن "الحاوية" تحتفظ بإشارة إلى المكون "الغبي" في الخاصية الثابتة WrappedComponent :

لذا بدلاً من:

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

يمكنك تجنب التصدير المزدوج باستخدام WrappedComponent حيث تحتاج إلى الإصدار "dumb":

// 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 خاصية مستقرة جدًا نظرًا لوجودها في المستندات. هل تنصح بعدم استخدامه بهذه الطريقة لأي سبب؟

ما أفعله هو تصدير المكون غير المغلف ، وكذلك تصدير mapStateToProps و mapDispatchToProps حتى أتمكن من اختبار هذه الوظائف. هنا مثال:

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" }
        ])
    });
});
هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

benoneal picture benoneal  ·  3تعليقات

cloudfroster picture cloudfroster  ·  3تعليقات

mickeyreiss-visor picture mickeyreiss-visor  ·  3تعليقات

CellOcean picture CellOcean  ·  3تعليقات

jimbolla picture jimbolla  ·  3تعليقات