كيف تبدأ اختبار تطبيقات React باستخدام مكتبة اختبار React و Jest

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

في عالم React ، توجد مكتبة مذهلة تسمى the react-testing-libraryوالتي تساعدك على اختبار تطبيقات React بشكل أكثر كفاءة. يمكنك استخدامه مع Jest.

في هذه المقالة ، سنرى الخطوات الثماني البسيطة التي يمكنك اتخاذها لبدء اختبار تطبيقات React الخاصة بك مثل الرئيس.

  • المتطلبات الأساسية
  • الأساسيات
  • ما هي مكتبة اختبار التفاعل؟
  • 1. كيفية إنشاء لقطة اختبارية؟
  • 2. اختبار عناصر DOM
  • 3. أحداث الاختبار
  • 4. اختبار الإجراءات غير المتزامنة
  • 5. اختبار رد الفعل
  • 6. اختبار سياق رد الفعل
  • 7. اختبار جهاز التوجيه React
  • 8. اختبار طلب HTTP
  • افكار اخيرة
  • الخطوات التالية

المتطلبات الأساسية

يفترض هذا البرنامج التعليمي أن لديك على الأقل فهم أساسي لـ React. سأركز فقط على جزء الاختبار.

وللمتابعة ، عليك استنساخ المشروع من خلال تشغيله في جهازك:

 git clone //github.com/ibrahima92/prep-react-testing-library-guide 

بعد ذلك ، قم بتشغيل:

 yarn 

أو إذا كنت تستخدم NPM:

npm install 

وهذا كل شيء! الآن دعنا نتعمق في بعض الأساسيات.

الأساسيات

سيتم استخدام بعض الأشياء الرئيسية كثيرًا في هذه المقالة ، ويمكن أن يساعدك فهم دورها في فهمك.

it or test: يصف الاختبار نفسه. يأخذ كمعلمات اسم الاختبار والوظيفة التي تجري الاختبارات.

expect: الشرط الذي يجب أن يجتازه الاختبار. سيقارن المعلمة المستلمة بالمطابق.

a matcher: وظيفة يتم تطبيقها على الحالة المتوقعة.

render: الطريقة المستخدمة لتصيير مكون معين.

import React from 'react' import {render} from '@testing-library/react' import App from './App' it('should take a snapshot', () => { const { asFragment } = render() expect(asFragment()).toMatchSnapshot() }) }); 

كما ترى ، نصف الاختبار it، ثم استخدمه renderلعرض مكون التطبيق ونتوقع أن asFragment()المطابقات toMatchSnapshot()(المطابق المقدم من jest-dom).

بالمناسبة ، renderتُرجع الطريقة عدة طرق يمكننا استخدامها لاختبار ميزاتنا. استخدمنا أيضًا التدمير للحصول على الطريقة.

بعد قولي هذا ، دعنا ننتقل ونتعرف على المزيد حول مكتبة اختبار React في القسم التالي.

ما هي مكتبة اختبار التفاعل؟

مكتبة اختبار React عبارة عن حزمة خفيفة الوزن جدًا تم إنشاؤها بواسطة Kent C. Dodds. إنه بديل للإنزيم ويوفر وظائف مفيدة خفيفة فوق react-domو react-dom/test-utils.

مكتبة اختبار React هي مكتبة اختبار DOM ، مما يعني أنه بدلاً من التعامل مع مثيلات مكونات React المعروضة ، فإنها تتعامل مع عناصر DOM وكيفية تصرفها أمام مستخدمين حقيقيين.

إنها مكتبة رائعة ، ومن السهل (نسبيًا) البدء في استخدامها ، وتشجع ممارسات الاختبار الجيدة. ملاحظة - يمكنك أيضًا استخدامه بدون Jest.

"كلما كانت اختباراتك مشابهة للطريقة التي يتم بها استخدام برنامجك ، زادت الثقة التي يمكن أن تمنحك إياها."

لذا ، لنبدأ في استخدامه في القسم التالي. بالمناسبة ، لا تحتاج إلى تثبيت أي حزم ، حيث create-react-appتأتي مع المكتبة وتبعياتها.

1. كيفية إنشاء لقطة اختبارية

اللقطة ، كما يوحي الاسم ، تسمح لنا بحفظ لقطة لمكون معين. إنه يساعد كثيرًا عند التحديث أو القيام ببعض إعادة البناء ، وتريد الحصول على التغييرات أو مقارنتها.

الآن ، دعنا نأخذ لقطة App.jsللملف.

  • App.test.js
import React from 'react' import {render, cleanup} from '@testing-library/react' import App from './App' afterEach(cleanup) it('should take a snapshot', () => { const { asFragment } = render() expect(asFragment()).toMatchSnapshot() }) }); 

لأخذ لقطة ، علينا أولاً استيراد renderو cleanup. سيتم استخدام هاتين الطريقتين كثيرًا في هذه المقالة.

render، كما قد تتخيل ، يساعد في تصيير مكون React. و cleanupيتم تمرير كمعلمة إلى afterEachأن مجرد تنظيف كل شيء بعد كل اختبار لتسرب الذاكرة تجنب.

بعد ذلك ، يمكننا تقديم مكون التطبيق مع renderوالعودة asFragmentكقيمة معاد من الطريقة. وأخيرًا ، تأكد من تطابق جزء مكون التطبيق مع اللقطة.

الآن ، لإجراء الاختبار ، افتح الجهاز الطرفي وانتقل إلى جذر المشروع وقم بتشغيل الأمر التالي:

 yarn test 

أو إذا كنت تستخدم npm:

 npm test 

ونتيجة لذلك، فإنه سيتم إنشاء مجلد جديد __snapshots__وملف App.test.js.snapفي srcالتي سوف تبدو مثل هذا:

  • App.test.js.snap
// Jest Snapshot v1, //goo.gl/fbAQLP exports[`Take a snapshot should take a snapshot 1`] = ` 

Testing

`;

وإذا أجريت تغييرًا آخر App.js، فسيفشل الاختبار ، لأن اللقطة لن تتطابق مع الحالة. لجعله يمر ، فقط اضغط uلتحديثه. وستحصل على اللقطة المحدثة بتنسيق App.test.js.snap.

الآن ، دعنا ننتقل ونبدأ في اختبار عناصرنا.

2. اختبار عناصر DOM

لاختبار عناصر DOM الخاصة بنا ، علينا أولاً إلقاء نظرة على TestElements.jsالملف.

  • TestElements.js
import React from 'react' const TestElements = () => { const [counter, setCounter] = React.useState(0) return (  

{ counter }

setCounter(counter + 1)}> Up setCounter(counter - 1)}>Down ) } export default TestElements

هنا ، الشيء الوحيد الذي عليك الاحتفاظ به هو data-testid. سيتم استخدامه لتحديد هذه العناصر من ملف الاختبار. الآن ، لنكتب اختبار الوحدة:

اختبر إذا كان العداد يساوي 0:

TestElements.test.js

import React from 'react'; import { render, cleanup } from '@testing-library/react'; import TestElements from './TestElements' afterEach(cleanup); it('should equal to 0', () => { const { getByTestId } = render(); expect(getByTestId('counter')).toHaveTextContent(0) }); 

كما ترى ، فإن بناء الجملة مشابه تمامًا للاختبار السابق. الاختلاف الوحيد هو أننا نستخدمه getByTestIdلتحديد العناصر الضرورية (تذكر data-testid) والتحقق مما إذا كانت قد اجتازت الاختبار. بمعنى آخر ، نتحقق مما إذا كان محتوى النص

{ counter }

يساوي 0.

اختبر ما إذا كانت الأزرار ممكنة أو معطلة:

TestElements.test.js (أضف كتلة التعليمات البرمجية التالية إلى الملف)

 it('should be enabled', () => { const { getByTestId } = render(); expect(getByTestId('button-up')).not.toHaveAttribute('disabled') }); it('should be disabled', () => { const { getByTestId } = render(); expect(getByTestId('button-down')).toBeDisabled() }); 

هنا ، كالعادة ، نستخدم getByTestIdلتحديد العناصر والتحقق من الاختبار الأول إذا كان الزر له disabledسمة. وللثانية ، إذا تم تعطيل الزر أم لا.

وإذا قمت بحفظ الملف أو تشغيله مرة أخرى في جهازك yarn test، فسيتم اجتياز الاختبار.

مبروك! لقد نجح اختبارك الأول!

مبروك

الآن ، دعنا نتعلم كيفية اختبار حدث في القسم التالي.

3. أحداث الاختبار

Before writing our unit tests, let's first check what the TestEvents.js looks like.

  • TestEvents.js
import React from 'react' const TestEvents = () => { const [counter, setCounter] = React.useState(0) return (  

{ counter }

setCounter(counter + 1)}> Up setCounter(counter - 1)}>Down ) } export default TestEvents

Now, let's write the tests.

Test if the counter increments and decrements correctly when we click on buttons:

TestEvents.test.js

import React from 'react'; import { render, cleanup, fireEvent } from '@testing-library/react'; import TestEvents from './TestEvents' afterEach(cleanup); it('increments counter', () => { const { getByTestId } = render(); fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }); it('decrements counter', () => { const { getByTestId } = render(); fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') }); 

As you can see, these two tests are very similar except the expected text content.

The first test fires a click event with fireEvent.click() to check if the counter increments to 1 when the button is clicked.

And the second one checks if the counter decrements to -1 when the button is clicked.

fireEvent has several methods you can use to test events, so feel free to dive into the documentation to learn more.

Now that we know how to test events, let's move on and learn in the next section how to deal with asynchronous actions.

4. Testing asynchronous actions

An asynchronous action is something that can take time to complete. It can be an HTTP request, a timer, and so on.

Now, let's check the TestAsync.js file.

  • TestAsync.js
import React from 'react' const TestAsync = () => { const [counter, setCounter] = React.useState(0) const delayCount = () => ( setTimeout(() => { setCounter(counter + 1) }, 500) ) return (  

{ counter }

Up setCounter(counter - 1)}>Down ) } export default TestAsync

Here, we use setTimeout() to delay the incrementing event by 0.5s.

Test if the counter is incremented after 0.5s:

TestAsync.test.js

import React from 'react'; import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react'; import TestAsync from './TestAsync' afterEach(cleanup); it('increments counter after 0.5s', async () => { const { getByTestId, getByText } = render(); fireEvent.click(getByTestId('button-up')) const counter = await waitForElement(() => getByText('1')) expect(counter).toHaveTextContent('1') }); 

To test the incrementing event, we first have to use async/await to handle the action because, as I said earlier, it takes time to complete.

Next, we use a new helper method getByText(). This is similar to getByTestId(), except that getByText() selects the text content instead of id or data-testid.

Now, after clicking to the button, we wait for the counter to be incremented with waitForElement(() => getByText('1')). And once the counter incremented to 1, we can now move to the condition and check if the counter is effectively equal to 1.

That being said, let's now move to more complex test cases.

Are you ready?

جاهز

5. Testing React Redux

If you're new to React Redux, this article might help you. Otherwise, let's check what the TestRedux.js looks like.

  • TestRedux.js
import React from 'react' import { connect } from 'react-redux' const TestRedux = ({counter, dispatch}) => { const increment = () => dispatch({ type: 'INCREMENT' }) const decrement = () => dispatch({ type: 'DECREMENT' }) return (  

{ counter }

Up Down ) } export default connect(state => ({ counter: state.count }))(TestRedux)

And for the reducer:

  • store/reducer.js
export const initialState = { count: 0, } export function reducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { count: state.count + 1, } case 'DECREMENT': return { count: state.count - 1, } default: return state } } 

As you can see, there is nothing fancy – it's just a basic Counter Component handled by React Redux.

Now, let's write the unit tests.

Test if the initial state is equal to 0:

TestRedux.test.js

import React from 'react' import { createStore } from 'redux' import { Provider } from 'react-redux' import { render, cleanup, fireEvent } from '@testing-library/react'; import { initialState, reducer } from '../store/reducer' import TestRedux from './TestRedux' const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {} ) => { return { ...render({component}), store, } } afterEach(cleanup); it('checks initial state is equal to 0', () => { const { getByTestId } = renderWithRedux() expect(getByTestId('counter')).toHaveTextContent('0') }) 

There are a couple of things we need to import to test React Redux. And here, we create our own helper function renderWithRedux() to render the component since it will be used several times.

renderWithRedux() receives as parameters the component to render, the initial state, and the store. If there is no store, it will create a new one, and if it doesn't receive an initial state or a store, it returns an empty object.

Next, we use render() to render the component and pass the store to the Provider.

That being said, we can now pass the component TestRedux to renderWithRedux() to test if the counter is equal to 0.

Test if the counter increments and decrements correctly:

TestRedux.test.js (add the following code block to the file)

it('increments the counter through redux', () => { const { getByTestId } = renderWithRedux(, {initialState: {count: 5} }) fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('6') }) it('decrements the counter through redux', () => { const { getByTestId} = renderWithRedux(, { initialState: { count: 100 }, }) fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('99') }) 

To test the incrementing and decrementing events, we pass an initial state as a second argument to renderWithRedux(). Now, we can click on the buttons and test if the expected result matches the condition or not.

Now, let's move to the next section and introduce React Context.

React Router and Axios will come next – are you still with me?

بالطبع

6. Testing React Context

If you're new to React Context, check out this article first. Otherwise, let's check the TextContext.js file.

  • TextContext.js
import React from "react" export const CounterContext = React.createContext() const CounterProvider = () => { const [counter, setCounter] = React.useState(0) const increment = () => setCounter(counter + 1) const decrement = () => setCounter(counter - 1) return (    ) } export const Counter = () => { const { counter, increment, decrement } = React.useContext(CounterContext) return (  

{ counter }

Up Down ) } export default CounterProvider

Now, the counter state is managed through React Context. Let's write the unit test to check if it behaves as expected.

Test if the initial state is equal to 0:

TextContext.test.js

import React from 'react' import { render, cleanup, fireEvent } from '@testing-library/react' import CounterProvider, { CounterContext, Counter } from './TestContext' const renderWithContext = ( component) => { return { ...render(  {component} ) } } afterEach(cleanup); it('checks if initial state is equal to 0', () => { const { getByTestId } = renderWithContext() expect(getByTestId('counter')).toHaveTextContent('0') }) 

As in the previous section with React Redux, here we use the same approach, by creating a helper function renderWithContext() to render the component. But this time, it receives only the component as a parameter. And to create a new context, we pass CounterContext to the Provider.

Now, we can test if the counter is initially equal to 0 or not.

Test if the counter increments and decrements correctly:

TextContext.test.js (add the following code block to the file)

 it('increments the counter', () => { const { getByTestId } = renderWithContext() fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }) it('decrements the counter', () => { const { getByTestId} = renderWithContext() fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') }) 

As you can see, here we fire a click event to test if the counter increments correctly to 1 and decrements to -1.

That being said, we can now move to the next section and introduce React Router.

7. Testing React Router

If you want to dive into React Router, this article might help you. Otherwise, let's check the TestRouter.js file.

  • TestRouter.js
import React from 'react' import { Link, Route, Switch, useParams } from 'react-router-dom' const About = () =>

About page

const Home = () =>

Home page

const Contact = () => { const { name } = useParams() return

{name}

} const TestRouter = () => { const name = 'John Doe' return ( Home About Contact ) } export default TestRouter

Here, we have some components to render when navigating the Home page.

Now, let's write the tests:

  • TestRouter.test.js
import React from 'react' import { Router } from 'react-router-dom' import { render, fireEvent } from '@testing-library/react' import { createMemoryHistory } from 'history' import TestRouter from './TestRouter' const renderWithRouter = (component) => { const history = createMemoryHistory() return { ...render (  {component}  ) } } it('should render the home page', () => { const { container, getByTestId } = renderWithRouter() const navbar = getByTestId('navbar') const link = getByTestId('home-link') expect(container.innerHTML).toMatch('Home page') expect(navbar).toContainElement(link) }) 

To test React Router, we have to first have a navigation history to start with. Therefore we use createMemoryHistory() to well as the name guessed to create a navigation history.

Next, we use our helper function renderWithRouter() to render the component and pass history to the Router component. With that, we can now test if the page loaded at the start is the Home page or not. And if the navigation bar is loaded with the expected links.

Test if it navigates to other pages with the parameters when we click on links:

TestRouter.test.js (add the following code block to the file)

it('should navigate to the about page', ()=> { const { container, getByTestId } = renderWithRouter() fireEvent.click(getByTestId('about-link')) expect(container.innerHTML).toMatch('About page') }) it('should navigate to the contact page with the params', ()=> { const { container, getByTestId } = renderWithRouter() fireEvent.click(getByTestId('contact-link')) expect(container.innerHTML).toMatch('John Doe') }) 

Now, to check if the navigation works, we have to fire a click event on the navigation links.

For the first test, we check if the content is equal to the text in the About Page, and for the second, we test the routing params and check if it passed correctly.

We can now move to the final section and learn how to test an Axios request.

We're almost done!

لا يزال هنا

8. Testing HTTP Request

As usual, let's first see what the TextAxios.js file looks like.

  • TextAxios.js
import React from 'react' import axios from 'axios' const TestAxios = ({ url }) => { const [data, setData] = React.useState() const fetchData = async () => { const response = await axios.get(url) setData(response.data.greeting) } return (  Load Data { data ? {data} : 

Loading...

} ) } export default TestAxios

As you can see here, we have a simple component that has a button to make a request. And if the data is not available, it will display a loading message.

Now, let's write the tests.

Test if the data are fetched and displayed correctly:

TextAxios.test.js

import React from 'react' import { render, waitForElement, fireEvent } from '@testing-library/react' import axiosMock from 'axios' import TestAxios from './TestAxios' jest.mock('axios') it('should display a loading text', () => { const { getByTestId } = render() expect(getByTestId('loading')).toHaveTextContent('Loading...') }) it('should load and display the data', async () => { const url = '/greeting' const { getByTestId } = render() axiosMock.get.mockResolvedValueOnce({ data: { greeting: 'hello there' }, }) fireEvent.click(getByTestId('fetch-data')) const greetingData = await waitForElement(() => getByTestId('show-data')) expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) expect(greetingData).toHaveTextContent('hello there') }) 

This test case is a bit different because we have to deal with an HTTP request. And to do that, we have to mock an axios request with the help of jest.mock('axios').

Now, we can use axiosMock and apply a get() method to it. Finally we will use the Jest function mockResolvedValueOnce() to pass the mocked data as a parameter.

With that, now for the second test we can click to the button to fetch the data and use async/await to resolve it. And now we have to test 3 things:

  1. If the HTTP request has been done correctly
  2. If the HTTP request has been done with the url
  3. If the data fetched matches the expectation.

And for the first test, we just check if the loading message is displayed when we have no data to show.

That being said, we're now done with the 8 simple steps to start testing your React Apps.

Don't be scared to test anymore.

غير خائف

Final Thoughts

مكتبة اختبار React عبارة عن حزمة رائعة لاختبار تطبيقات React. يتيح لنا الوصول إلى أدوات jest-domالمطابقة التي يمكننا استخدامها لاختبار مكوناتنا بشكل أكثر كفاءة ومع الممارسات الجيدة. نأمل أن تكون هذه المقالة مفيدة ، وسوف تساعدك على إنشاء تطبيقات React قوية في المستقبل.

يمكنك العثور على المشروع النهائي هنا

شكرا لقراءته!

اقرأ المزيد من المقالات - اشترك في رسالتي الإخبارية - تابعني على تويتر

يمكنك قراءة مقالات أخرى مثل هذه على مدونتي.

الخطوات التالية

React Testing Library مستندات مكتبة

ورقة الغش في مكتبة اختبار التفاعل

ورقة غش لمطابقات Jest DOM

Jest Docs