تست برنامه های ری اکتی با پکیج Enzyme

19 اردیبهشت 1398
درسنامه درس 25 از سری آموزش react (ری اکت)
React-enzyme

در درس قبلی با استفاده از کتابخانه react-addons-test.utils برای کامپوننت Timeline تست نوشتیم. اما همان طور که دیدید این کتابخانه خیلی سطح پایین بوده و کار با آن مشکل است. برای این منظور از کتابخانه Enzyme که یکی از بهترین کتابخانه های کمکی برای تست کامپوننت های ری اکت است، استفاده می کنیم. این کتابخانه توسط تیم Aribnb تهیه شده و یک Api سطح بالا و خیلی راحت را برای تست کامپوننت های ری اکت ارائه می دهد.

در این درس کامپوننت <Timeline/> خود را با استفاده از Enzyme تست می کنیم.

استفاده از Enzyme

کتابخانه Enzyme فرآیند تست کامپوننت ها را راحت تر کرده و همچنین خوانایی تست را هم بالا می برد. در درس قبلی کامپوننت Timeline را مطابق زیر تست کردیم:

import React from 'react';
import TestUtils from 'react-addons-test-utils';

import Timeline from '../Timeline';

describe('Timeline', () => {

  it('wraps content in a div with .notificationsFrame class', () => {
    const wrapper = TestUtils.renderIntoDocument(<Timeline />);
    TestUtils
      .findRenderedDOMComponentWithClass(wrapper, 'notificationsFrame');
  });

})

هر چند که این کد کار می کند، اما کار با آن کمی پیچیده است. حال می خواهیم تست بالا را توسط Enzyme انجام دهیم. Enzyme به جای اینکه کل ساختار درختی کامپوننت را تست کنیم، می توانیم فقط خروجی کامپوننت را تست کنیم و هیچکدام از فرزندان کامپوننت رندر نخواهد شد. به این کار رندر سطحی (shallow) هم می گویند.

Enzyme تست های سطحی را خیلی راحت انجام می دهد. برای شروع از تابع shallow که توسط Enzyme ارائه شده، استفاده کرده و آنرا به کامپوننت مان متصل می کنیم.

حال فایل src/Component/Timeline/__test__/Timeline-test.js  را باز کرده و تابع shallow را از کتابخانه Enzyme به داخل این فایل وارد کنید.

import React from 'react';
import { shallow } from 'enzyme';

describe('Timeline', () => {
  it('wraps content in a div with .notificationsFrame class', () => {
    // our tests
  });
})

رندر سطحی توسط react-addons-test-utils هم به خوبی پشتیبانی می شود. در حقیقت Enzyme هم از همین ویژگی استفاده می کند. در زیر توسط پکیج react-addons-test-utils رندر سطحی را انجام می دهیم.

const renderer = ReactTestUtils.createRenderer();
renderer.render(<Timeline />)
const result = renderer.getRenderOutput();

حال برای رندر کامپوننت کافی است که از متد shallow استفاده کرد و نتیجه را در یک متغیر ذخیره کنیم. سپس از کامپوننت های رندر شده می توان برای عناصری که در داخل Dom مجازی رندر می شوند، استفاده کرد.

کل فرآیند با دو خط انجام می شود:

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

import Timeline from '../Timeline';

describe('Timeline', () => {
  let wrapper;

  it('wraps content in a div with .notificationsFrame class', () => {
    wrapper = shallow(<Timeline />);
    expect(wrapper.find('.notificationsFrame').length).toEqual(1);
  });

  it('has a title of Timeline', () => {
    wrapper = mount(<Timeline />)
    expect(wrapper.find('.title').text()).toBe("Timeline")
  })

  describe('search button', () => {
    let search;
    beforeEach(() => wrapper = mount(<Timeline />))
    beforeEach(() => search = wrapper.find('input.searchInput'))

    it('starts out hidden', () => {  
      expect(search.hasClass('active')).toBeFalsy()
    })
    it('becomes visible after being clicked on', () => {
      const icon = wrapper.find('.searchIcon')
      icon.simulate('click')
      expect(search.hasClass('active')).toBeTruthy()
    })
  })

  describe('status updates', () => {
    it('has 4 status updates at minimum', () => {
      wrapper = shallow(<Timeline />)
      expect(
        wrapper.find('ActivityItem').length
      ).toBeGreaterThan(3)
    })
  })

})

برای تست از همان حالت قبلی یعنی استفاده از دستورات npm test یا yarn test استفاده می کنیم.

yarn test
استفاده از Enzyme برای تست کامپوننت های ری اکت
استفاده از Enzyme برای تست کامپوننت های ری اکت

همان طور که می بینید این تست خوانایی بیشتری نسبت به قبلی دارد.

حال یکی دیگر از پیش فرض ها که در درس قبلی درباره آن صحبت کردیم، را تست می کنیم. بقیه تست های این درس را با بلوک های describe و it سازمان دهی خواهیم کرد.

import React from 'react';
import { shallow } from 'enzyme';

import Timeline from '../Timeline';

describe('Timeline', () => {
  let wrapper;

  it('wraps content in a div with .notificationsFrame class', () => {
    wrapper = shallow(<Timeline />);
    expect(wrapper.find('.notificationsFrame').length).toEqual(1);
  });

  it('has a title of Timeline')

  describe('search button', () => {
    it('starts out hidden')
    it('becomes visible after being clicked on')
  })

  describe('status updates', () => {
    it('has 4 status updates at minimum')
  })

})

اگر توسعه تست محور (یا TDD) زیر را دنبال کنیم، می توانیم این پیش فرض ها را نوشته و سپس یک کامپوننت که عملیات تست با موفقیت روی آن انجام می شود را بنویسیم.

حال می خواهیم یک تست برای کامپوننت Timeline بنویسیم.

عنوان تست ساده است. ابتدا عنوان (title) عنصر را بررسی می کنیم تا ببینیم که مقدارش برابر Timeline است یا خیر.

فرض می کنیم که عنصرهای مورد نظر یک کلاس Title دارند که از آن طریق می توان به عنوان آنها دسترسی داشت. بنابراین برای استفاده از کلاس title باید توسط متد ()find مربوط به کتابخانه Enzyme کامپوننت مورد نظرمان را انتخاب کنیم.

نکته: چون کامپوننت Header فرزند کامپوننت Timeline است. نمی توانیم از متد ()shallow استفاده کنیم و در عوض باید از متد ()mount که توسط Enzyme ارائه شده، استفاده کرد.

shallow یا Mount؟

تابع رندر shallow فقط کامپوننتی که مشغول تست آن هستیم را رندر می کند و عنصرهای فرزند آن را رندر نمی کند.

در عوض باید از متد ()mount کامپوننت استفاده کنیم، چون کامپوننت Header در Dom صفحه قابل دسترس نیست.

در انتهای این مقاله به توابع دیگر Enzyme هم نگاهی می اندازیم. حال برای تست عنوان عنصر، کد زیر را می نویسیم:

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

import Timeline from '../Timeline';

describe('Timeline', () => {
  let wrapper;

  it('wraps content in a div with .notificationsFrame class', () => {
    wrapper = shallow(<Timeline />);
    expect(wrapper.find('.notificationsFrame').length).toEqual(1);
  });

  it('has a title of Timeline', () => {
    wrapper = mount(<Timeline />) // notice the `mount`
    expect(wrapper.find('.title').text()).toBe("Timeline")
  })
})

با اجرای دو تست فوق، می بینید که این دو نمونه مورد انتظار ما با موفقیت Pass می شود:

اجرای رندر سطحی کامپوننت ها با Enzyme

در مرحله بعد، تست دکمه جستجوی را بروزرسانی می کنیم. Enzyme اینترفیس های ساده ای را برای مدیریت این وظایف ارائه می دهد. حال باید ببینیم که چطور می شود برای آیکن جستجو یک تست نوشت؟

بار دیگر یادآوری می کنیم که چون می خواهیم یک عنصر فرزند کامپوننت Timelineرا تست کنیم، باید از متد ()mount را بکار بگیریم. همچنین چون می خواهیم در یک بلوک ()describe دو تست بنویسیم، از یک متد کمکی before برای هرکدام از تست ها بهره می بریم.

از طرفی  چون می خواهیم از عنصر input.SearchInput برای هر دو تست استفاده کنیم، باید از متد ()find برای جستجوی آن عنصر استفاده کنیم:

describe('Timeline', () => {
  let wrapper;
  // ...
  describe('search button', () => {
    let search;
    beforeEach(() => wrapper = mount(<Timeline />))
    beforeEach(() => search = wrapper.find('input.searchInput'))
    // ...
  })
})

برای تست پنهان بودن فیلد جستجو، باید ببینیم که آیا کلاس active به آن اعمال شده یا خیر.

Enzyme متدی به نام ()hasClass دارد که توسط آن می توانیم بفهمیم که آیا کامپوننت کلاس مورد نظر را دارد یا خیر.

describe('Timeline', () => {
  let wrapper;
  // ...
  describe('search button', () => {
    let search;
    beforeEach(() => wrapper = mount(<Timeline />))
    beforeEach(() => search = wrapper.find('input.searchInput'))

    it('starts out hidden', () => {  
      expect(search.hasClass('active')).toBeFalsy()
    })
    it('becomes visible after being clicked on')
    // ...
  })
})

نکته مهم در تست دوم این است که ما باید روی عنصر آیکن کلیک کنیم. پس قبل از هر کاری ابتدا باید آنرا پیدا کنیم. این کار را می توان توسط کلاس searchIcon. روی wrapper انجام داد.

it('becomes visible after being clicked on', () => {
  const icon = wrapper.find('.searchIcon')
})

یک آیکنی داریم که می توانیم عملیات کلیک روی عنصر را روی آن شبیه سازی کنیم. دقت کنید که متد ()onClick فقط توسط رویداد مرورگر اجرا می شود. در اینجا، کلیک روی یک عنصر در واقع رویدادی است که توسط یک کامپوننت اتفاق می افتد.

در واقع به جای کنترل یک ماوس یا فراخوانی click روی عنصر، اجرای یک رویداد را شبیه سازی می کنیم.

در این مثال رویداد کلیک را بررسی می کنیم.

سپس با اجرای متد simulate روی icon، این رویداد را ایجاد می کنیم:

it('becomes visible after being clicked on', () => {
  const icon = wrapper.find('.searchIcon')
  icon.simulate('click')
})

حال می توانیم یک پیش فرض یا توضیح برای کامپوننت search که یک کلاس active دارد، تنظیم کنیم.

it('becomes visible after being clicked on', () => {
  const icon = wrapper.find('.searchIcon')
  icon.simulate('click')
  expect(search.hasClass('active')).toBeTruthy()
})

آخرین پیش فرض برای کامپوننت Timeline این است که باید حداقل چهار بروزرسانی وضعیت داشته باشیم.

بعد از اینکه این عناصر را روی کامپوننت Timeline قرار دادیم، می توانیم عملیات رندر سطحی (shallow) را روی کامپوننت اجرا کنیم. به علاوه چون هر کدام از این عناصر یک کامپوننت سفارشی هستند، می توانیم به دنبال لیستی از کامپوننت های خاص از نوع ActivityItem باشیم:

describe('status updates', () => {
  it('has 4 status updates at minimum', () => {
    wrapper = shallow(<Timeline />)
    // ... 
  })
})

حال می توانیم برای تعداد کامپوننت های ActivityItem  یک لیست، تست انجام می دهیم. ما پیش فرض مان را اینگونه تعریف می کنیم که لیست حداقل چهار آیتم دارد:

describe('status updates', () => {
  it('has 4 status updates at minimum', () => {
    wrapper = shallow(<Timeline />)
    expect(
      wrapper.find('ActivityItem').length
    ).toBeGreaterThan(3)
  })
})

در زیر کدهای کامل مربوط به تست کامپوننت Timeline را مشاهده می کنیم:

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

import Timeline from '../Timeline';

describe('Timeline', () => {
  let wrapper;

  it('wraps content in a div with .notificationsFrame class', () => {
    wrapper = shallow(<Timeline />);
    expect(wrapper.find('.notificationsFrame').length).toEqual(1);
  });

  it('has a title of Timeline', () => {
    wrapper = mount(<Timeline />)
    expect(wrapper.find('.title').text()).toBe("Timeline")
  })

  describe('search button', () => {
    let search;
    beforeEach(() => wrapper = mount(<Timeline />))
    beforeEach(() => search = wrapper.find('input.searchInput'))

    it('starts out hidden', () => {  
      expect(search.hasClass('active')).toBeFalsy()
    })
    it('becomes visible after being clicked on', () => {
      const icon = wrapper.find('.searchIcon')
      icon.simulate('click')
      expect(search.hasClass('active')).toBeTruthy()
    })
  })

  describe('status updates', () => {
    it('has 4 status updates at minimum', () => {
      wrapper = shallow(<Timeline />)
      expect(
        wrapper.find('ActivityItem').length
      ).toBeGreaterThan(3)
    })
  })

})

کار متد ()find چیست؟

قبل از اینکه درس امروز را به پایان برسانیم، می خواهیم نگاهی به اینترفیس یک کامپوننت رندر سطحی شده Enzyme بیندازیم (در مثال ها، آبجکت Wrapper).

مستندات Enzyme خیلی خوب نوشته شده است. بنابراین حتما نگاهی مختصر به آن بیندازیم.

هنگام استفاده از متد ()find یک سلکتور به آن پاس داده و این متد یک نمونه از shallowWrapper را که در واقع همان نودهای پیدا شده است، بر می گرداند.

متد ()find به عنوان پارامتر می تواند یک رشته تابع، یا یک آبجکت را دریافت کند. هنگامی که یک رشته به متد ()find پاس می دهیم، می توانیم از سلکتورهای CSS یا نام یک کامپوننت استفاده کنیم. برای مثال :

wrapper.find('div.link');
wrapper.find('Link')

همچنین می توانیم متد سازنده یک کامپوننت را به متد find ارسال نماییم، مانند زیر:

import { Link } from 'react-router';
// ...
wrapper.find(Link)

در انتها می توانیم یک آبجکت را به این متد پاس دهیم، که در این صورت توسط کلید و مقادیر این آبجکت، عناصر مورد نظر را انتخاب می کند. برای مثال:

wrapper.find({to: '/login'});

مقدار برگشتی از متد یک ShallowWrapper که در واقع یک نوعی از wrapper است، می باشد.(می توانیم wrapper های رندر شده و wrapper های سطحی داشته باشیم).

این نمونه های Wrapper برگشت داده شده، مجموعه ای از توابعی را ارائه می دهد که می توانیم برای پیدا کردن کامپوننت های فرزند از آنها استفاده کنیم، دقیقاً همانند روشی که از props و state و دیگر خصیصه های یک کامپوننت رندر شده از قبیل ()html و ()text استفاده می کردیم.

علاوه بر این می توانیم این توابع را به صورت زنجیره ای فراخوانی کنیم.

برای مثال کامپوننت <Link/>  را در نظر بگیرید. اگر بخواهیم تگ html لینک که کلاس link. دارد را پیدا کنیم، می توانیم از تست زیر برای اینکار استفاده کنیم:

// ...
it('displays a link tag with the Login text', () => {
  link = wrapper
        .find('Link')
        .find({to: '/login'})

  expect(link.html())
    .toBe('<a class="link">Login</a>')
});

در این درس اطلاعات زیادی را بررسی کردیم و دیدیم که چطور می شود توسط Enzyme با سرعت بیشتری اقدام به تست برنامه ها کرد. در درس آینده بحث درباره تست کامپوننت ها را ادامه داده و عملیات تست را در برنامه های مان یکپارچه می کنیم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش react (ری اکت) توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.