مقالات

نحوه تست قلاب های سفارشی برای React


اگر استفاده می کنید react@>=16.8، سپس می توانید از قلاب ها استفاده کنید و احتمالاً دارید
خودم چند تا سفارشی نوشتم شاید فکر کرده باشید که چگونه اعتماد به نفس داشته باشید
که قلاب شما در طول عمر برنامه شما به کار خود ادامه می دهد. من هم هستم
نه به ذکر قلاب سفارشی یکباره ای که فقط برای ساختن خودتان بیرون می آورید
بدنه کامپوننت کمتر و کد خود را سازماندهی کنید (آنها باید توسط شما پوشش داده شوند
تست های کامپوننت)، من در مورد آن قلاب قابل استفاده مجدد که ارسال کردید صحبت می کنم
github/npm (یا در مورد آن با بخش حقوقی خود صحبت کرده اید).

فرض کنید این قلاب سفارشی به نام داریم useUndo (با الهام از
useUndo از
هومر چن):

(توجه داشته باشید که درک این موضوع واقعاً مهم نیست، اما می توانید
اگر کنجکاو هستید، این را گسترش دهید):

پیاده سازی useUndo
import * as React from 'react'

const UNDO = 'UNDO'
const REDO = 'REDO'
const SET = 'SET'
const RESET = 'RESET'

function undoReducer(state, action) {
	const { past, present, future } = state
	const { type, newPresent } = action

	switch (action.type) {
		case UNDO: {
			if (past.length === 0) return state

			const previous = past[past.length - 1]
			const newPast = past.slice(0, past.length - 1)

			return {
				past: newPast,
				present: previous,
				future: [present, ...future],
			}
		}

		case REDO: {
			if (future.length === 0) return state

			const next = future[0]
			const newFuture = future.slice(1)

			return {
				past: [...past, present],
				present: next,
				future: newFuture,
			}
		}

		case SET: {
			if (newPresent === present) return state

			return {
				past: [...past, present],
				present: newPresent,
				future: [],
			}
		}

		case RESET: {
			return {
				past: [],
				present: newPresent,
				future: [],
			}
		}
		default: {
			throw new Error(`Unhandled action type: ${type}`)
		}
	}
}

function useUndo(initialPresent) {
	const [state, dispatch] = React.useReducer(undoReducer, {
		past: [],
		present: initialPresent,
		future: [],
	})

	const canUndo = state.past.length !== 0
	const canRedo = state.future.length !== 0
	const undo = React.useCallback(() => dispatch({ type: UNDO }), [])
	const redo = React.useCallback(() => dispatch({ type: REDO }), [])
	const set = React.useCallback(
		(newPresent) => dispatch({ type: SET, newPresent }),
		[],
	)
	const reset = React.useCallback(
		(newPresent) => dispatch({ type: RESET, newPresent }),
		[],
	)

	return { ...state, set, reset, undo, redo, canUndo, canRedo }
}

export default useUndo

فرض کنید می‌خواهیم یک تست برای این بنویسیم تا بتوانیم اعتماد به نفس را حفظ کنیم
ما تغییراتی ایجاد می کنیم و اشکالات را رفع می کنیم، عملکردهای موجود را خراب نمی کنیم. برای بدست آوردن
حداکثر اعتماد به نفس مورد نیاز ما، ما باید اطمینان حاصل کنیم که تست های ما
شبیه روش استفاده از نرم افزار است.
به یاد داشته باشید که نرم افزار همه چیز در مورد خودکار کردن چیزهایی است که ما نمی خواهیم یا نمی خواهیم
نمی توان به صورت دستی انجام داد. تست ها تفاوتی ندارند، بنابراین به این فکر کنید که چگونه این را آزمایش می کنید
به صورت دستی، سپس تست خود را بنویسید تا همین کار را انجام دهید.

اشتباهی که می بینم بسیاری از مردم مرتکب می شوند این است که فکر می کنند “خب، این آسان است
عملکرد درست، این چیزی است که ما در مورد قلاب ها دوست داریم. بنابراین نمی توانم فقط با آن تماس بگیرم
عملکرد و ادعا در خروجی؟ واحد FTW را آزمایش می کند!” آنها اشتباه نمی کنند. آن را است
فقط یک ویژگی است، اما از نظر فنی اینطور نیست
تمیز کردن عملکرد (قلاب های شما هستند
باید همچنان ناتوان باشد).
اگر تابع بود تمیز کردن، پس فراخوانی آن کار ساده ای خواهد بود و
اعتبار سنجی خروجی

اگر سعی کنید فقط تابع را در یک تست فراخوانی کنید، شکست خواهید خورد
قوانین قلاب و شما دریافت خواهید کرد
این خطا:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app
  See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

(من به هر سه دلیل ذکر شده این خطا را دریافت کردم 🙈)

حالا شما شاید شروع کنید به فکر کردن، “هی، اگر من فقط قلاب های داخلی React را مسخره می کنم
من به عنوان استفاده می کنم useState و useEffect سپس من هنوز هم می توانم آن را به عنوان یک تست کنمکارکرد.» اما برای عشق به همه چیز خالص، لطفاً این کار را نکنید
برای انجام آن اعتماد به نفس زیادی لازم است.

اما نگران نباشید، اگر لازم است این را به صورت دستی آزمایش کنید، اما فقط یک تابع را فراخوانی کنید، احتمالاً مؤلفه‌ای را می‌نویسید که از قلاب استفاده می‌کند و سپس تعامل برقرار می‌کند.
با آن جزء ارائه شده در صفحه (شاید با استفاده از
کتاب داستان). پس بیایید به جای آن این کار را انجام دهیم:

import * as React from 'react'
import useUndo from '../use-undo'

function UseUndoExample() {
	const { present, past, future, set, undo, redo, canUndo, canRedo } =
		useUndo('one')
	function handleSubmit(event) {
		event.preventDefault()
		const input = event.target.elements.newValue
		set(input.value)
		input.value = ''
	}

	return (
		div>
			div>
				button onClick={undo} disabled={!canUndo}>
					undo
				button>
				button onClick={redo} disabled={!canRedo}>
					redo
				button>
			div>
			form onSubmit={handleSubmit}>
				label htmlFor="newValue">New valuelabel>
				input type="text" id="newValue" />
				div>
					button type="submit">Submitbutton>
				div>
			form>
			div>Present: {present}div>
			div>Past: {past.join(', ')}div>
			div>Future: {future.join(', ')}div>
		div>
	)
}

export { UseUndoExample }

در اینجا تصویر آن است:

حاضر: یکی

گذشته:

آینده:

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

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'

import {UseUndoExample} from '../use-undo.example'

test('allows you to undo and redo', () => {
  render(UseUndoExample />)
  const present = screen.getByText(/present/i)
  const past = screen.getByText(/past/i)
  const future = screen.getByText(/future/i)
  const input = screen.getByLabelText(/new value/i)
  const submit = screen.getByText(/submit/i)
  const undo = screen.getByText(/undo/i)
  const redo = screen.getByText(/redo/i)

  // assert initial state
  expect(undo).toBeDisabled()
  expect(redo).toBeDisabled()
  expect(past).toHaveTextContent(`Past:`)
  expect(present).toHaveTextContent(`Present: one`)
  expect(future).toHaveTextContent(`Future:`)

  // add second value
  input.value = 'two'
  await userEvent.click(submit)

  // assert new state
  expect(undo).not.toBeDisabled()
  expect(redo).toBeDisabled()
  expect(past).toHaveTextContent(`Past: one`)
  expect(present).toHaveTextContent(`Present: two`)
  expect(future).toHaveTextContent(`Future:`)

  // add third value
  input.value = 'three'
  await userEvent.click(submit)

  // assert new state
  expect(undo).not.toBeDisabled()
  expect(redo).toBeDisabled()
  expect(past).toHaveTextContent(`Past: one, two`)
  expect(present).toHaveTextContent(`Present: three`)
  expect(future).toHaveTextContent(`Future:`)

  // undo
  await userEvent.click(undo)

  // assert "undone" state
  expect(undo).not.toBeDisabled()
  expect(redo).not.toBeDisabled()
  expect(past).toHaveTextContent(`Past: one`)
  expect(present).toHaveTextContent(`Present: two`)
  expect(future).toHaveTextContent(`Future: three`)

  // undo again
  await userEvent.click(undo)

  // assert "double-undone" state
  expect(undo).toBeDisabled()
  expect(redo).not.toBeDisabled()
  expect(past).toHaveTextContent(`Past:`)
  expect(present).toHaveTextContent(`Present: one`)
  expect(future).toHaveTextContent(`Future: two, three`)

  // redo
  await userEvent.click(redo)

  // assert undo + undo + redo state
  expect(undo).not.toBeDisabled()
  expect(redo).not.toBeDisabled()
  expect(past).toHaveTextContent(`Past: one`)
  expect(present).toHaveTextContent(`Present: two`)
  expect(future).toHaveTextContent(`Future: three`)

  // add fourth value
  input.value = 'four'
  await userEvent.click(submit)

  // assert final state (note the lack of "third")
  expect(undo).not.toBeDisabled()
  expect(redo).toBeDisabled()
  expect(past).toHaveTextContent(`Past: one, two`)
  expect(present).toHaveTextContent(`Present: four`)
  expect(future).toHaveTextContent(`Future:`)
})

من این نوع رویکرد را دوست دارم زیرا دنبال کردن آزمون نسبتاً آسان است و
می فهمم. در اکثر مواقع، آزمایش این گونه را به این روش توصیه می کنم
روی یک قلاب

با این حال، گاهی اوقات مؤلفه ای که باید بنویسید بسیار پیچیده است
و شما در نهایت با شکست در تست ها مواجه می شوید نه به این دلیل که قلاب شکسته است بلکه به این دلیل
مثالی که نوشتی کاملا ناامید کننده است.

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

در حال حاضر داشتن این اجزای نمونه احتمالاً ایده خوبی است به هر حال (آنها هستند
برای مثال برای یک کتاب داستان عالی است) اما گاهی اوقات
ممکن است خوب باشد که یک کمک کننده کوچک ایجاد کنید که واقعاً هیچ رابط کاربری نداشته باشد
به آن متصل شده و شما به طور مستقیم با مقدار بازگشتی قلاب تعامل دارید.

در اینجا نمونه ای از این است که برای ما چگونه به نظر می رسد useUndo قلاب:

import * as React from 'react'
import { render, act } from '@testing-library/react'
import useUndo from '../use-undo'

function setup(...args) {
	const returnVal = {}
	function TestComponent() {
		Object.assign(returnVal, useUndo(...args))
		return null
	}
	render(TestComponent />)
	return returnVal
}

test('allows you to undo and redo', () => {
	const undoData = setup('one')

	// assert initial state
	expect(undoData.canUndo).toBe(false)
	expect(undoData.canRedo).toBe(false)
	expect(undoData.past).toEqual([])
	expect(undoData.present).toEqual('one')
	expect(undoData.future).toEqual([])

	// add second value
	act(() => {
		undoData.set('two')
	})

	// assert new state
	expect(undoData.canUndo).toBe(true)
	expect(undoData.canRedo).toBe(false)
	expect(undoData.past).toEqual(['one'])
	expect(undoData.present).toEqual('two')
	expect(undoData.future).toEqual([])

	// add third value
	act(() => {
		undoData.set('three')
	})

	// assert new state
	expect(undoData.canUndo).toBe(true)
	expect(undoData.canRedo).toBe(false)
	expect(undoData.past).toEqual(['one', 'two'])
	expect(undoData.present).toEqual('three')
	expect(undoData.future).toEqual([])

	// undo
	act(() => {
		undoData.undo()
	})

	// assert "undone" state
	expect(undoData.canUndo).toBe(true)
	expect(undoData.canRedo).toBe(true)
	expect(undoData.past).toEqual(['one'])
	expect(undoData.present).toEqual('two')
	expect(undoData.future).toEqual(['three'])

	// undo again
	act(() => {
		undoData.undo()
	})

	// assert "double-undone" state
	expect(undoData.canUndo).toBe(false)
	expect(undoData.canRedo).toBe(true)
	expect(undoData.past).toEqual([])
	expect(undoData.present).toEqual('one')
	expect(undoData.future).toEqual(['two', 'three'])

	// redo
	act(() => {
		undoData.redo()
	})

	// assert undo + undo + redo state
	expect(undoData.canUndo).toBe(true)
	expect(undoData.canRedo).toBe(true)
	expect(undoData.past).toEqual(['one'])
	expect(undoData.present).toEqual('two')
	expect(undoData.future).toEqual(['three'])

	// add fourth value
	act(() => {
		undoData.set('four')
	})

	// assert final state (note the lack of "third")
	expect(undoData.canUndo).toBe(true)
	expect(undoData.canRedo).toBe(false)
	expect(undoData.past).toEqual(['one', 'two'])
	expect(undoData.present).toEqual('four')
	expect(undoData.future).toEqual([])
})

من احساس می کنم که این تست به ما اجازه می دهد تا به طور مستقیم با قلاب (که
به همین دلیل است act مورد نیاز است)، و این به ما امکان می دهد موارد بیشتری را پوشش دهیم
نوشتن نمونه های جزء دشوار است.

اکنون، گاهی اوقات شما قلاب های پیچیده تری دارید که باید منتظر تمسخر بمانید
HTTP درخواست تکمیل می‌کند یا می‌خواهید مؤلفه‌ای را که استفاده می‌کند «دوباره ارائه کنید».
قلاب با ساپورت های مختلف و غیره. هر یک از این موارد استفاده شما را پیچیده می کند
setup ویژگی یا مثال واقعی شما که آن را حتی بیشتر می کند
دامنه خاص و سخت به دنبال.

به همین دلیل است renderHook از
@testing-library/react
وجود دارد. اگر از این تست استفاده کنیم در اینجا به چه صورت خواهد بود @testing-library/react:

import { renderHook, act } from '@testing-library/react'
import useUndo from '../use-undo'

test('allows you to undo and redo', () => {
	const { result } = renderHook(() => useUndo('one'))

	// assert initial state
	expect(result.current.canUndo).toBe(false)
	expect(result.current.canRedo).toBe(false)
	expect(result.current.past).toEqual([])
	expect(result.current.present).toEqual('one')
	expect(result.current.future).toEqual([])

	// add second value
	act(() => {
		result.current.set('two')
	})

	// assert new state
	expect(result.current.canUndo).toBe(true)
	expect(result.current.canRedo).toBe(false)
	expect(result.current.past).toEqual(['one'])
	expect(result.current.present).toEqual('two')
	expect(result.current.future).toEqual([])

	// add third value
	act(() => {
		result.current.set('three')
	})

	// assert new state
	expect(result.current.canUndo).toBe(true)
	expect(result.current.canRedo).toBe(false)
	expect(result.current.past).toEqual(['one', 'two'])
	expect(result.current.present).toEqual('three')
	expect(result.current.future).toEqual([])

	// undo
	act(() => {
		result.current.undo()
	})

	// assert "undone" state
	expect(result.current.canUndo).toBe(true)
	expect(result.current.canRedo).toBe(true)
	expect(result.current.past).toEqual(['one'])
	expect(result.current.present).toEqual('two')
	expect(result.current.future).toEqual(['three'])

	// undo again
	act(() => {
		result.current.undo()
	})

	// assert "double-undone" state
	expect(result.current.canUndo).toBe(false)
	expect(result.current.canRedo).toBe(true)
	expect(result.current.past).toEqual([])
	expect(result.current.present).toEqual('one')
	expect(result.current.future).toEqual(['two', 'three'])

	// redo
	act(() => {
		result.current.redo()
	})

	// assert undo + undo + redo state
	expect(result.current.canUndo).toBe(true)
	expect(result.current.canRedo).toBe(true)
	expect(result.current.past).toEqual(['one'])
	expect(result.current.present).toEqual('two')
	expect(result.current.future).toEqual(['three'])

	// add fourth value
	act(() => {
		result.current.set('four')
	})

	// assert final state (note the lack of "third")
	expect(result.current.canUndo).toBe(true)
	expect(result.current.canRedo).toBe(false)
	expect(result.current.past).toEqual(['one', 'two'])
	expect(result.current.present).toEqual('four')
	expect(result.current.future).toEqual([])
})

متوجه خواهید شد که بسیار شبیه به رسم ماست setup تابع زیر کاپوت
@testing-library/react کاری بسیار شبیه به اصلی ما انجام می دهد setup
عملکرد بالا چند چیز دیگر که ما از آنها می گیریم @testing-library/react عبارتند از:

  • ابزاری برای “رندر مجدد” مؤلفه ای که قلاب را رندر می کند (برای آزمایش اثر
    تغییر وابستگی به عنوان مثال)
  • ابزاری برای “از بین بردن” مؤلفه ای که قلاب را رندر می کند (برای آزمایش اثر
    توابع تمیز کردن به عنوان مثال)
  • چندین ابزار ناهمزمان برای انتظار به طور نامحدود (برای آزمایش ناهمزمان
    منطق)

توجه کن، تو می تواند بیش از یک قلاب را به سادگی با فراخوانی همه هوک ها آزمایش کنید
می‌خواهید در تابع callback که به آن ارسال می‌کنید renderHook.

نوشتن یک مؤلفه “فقط تست” برای پشتیبانی از برخی از این موارد به مقدار مناسبی نیاز دارد
روی یک صفحه شابلون مستعد خطا و در نهایت می توانید زمان بیشتری را صرف نوشتن کنید و
آزمایش اجزای آزمایشی خود نسبت به قلابی که می خواهید آزمایش کنید.

برای روشن شدن، اگر من در حال نوشتن و تست خاص بودم useUndo قلاب، من
با استفاده از مثال در دنیای واقعی پیش بروید. من فکر می کنم این بهترین سازش را انجام می دهد
بین قابل درک بودن و پوشش موارد استفاده ما. اما وجود دارند
قطعا قلاب های پیچیده تر برای استفاده @testing-library/react بیشتر است
مفید


Source link

نوشته های مشابه

دیدگاهتان را بنویسید

همچنین ببینید
بستن
دکمه بازگشت به بالا