مقالات

هنگام آزمایش از لانه سازی خودداری کنید


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

توجه: منظور من این نیست که تودرتو به خودی خود بد است، بلکه این است که
به طور طبیعی استفاده از قلاب های آزمایشی (مانند beforeEach) به عنوان یک مکانیسم
برای استفاده مجدد از کد، که منجر به تست های پشتیبانی نشده می شود. لطفا به ادامه مطلب بروید…

در اینجا یک مؤلفه React است که می خواهم آزمایش کنم:

// login.js
import * as React from 'react'

function Login({ onSubmit }) {
	const [error, setError] = React.useState('')

	function handleSubmit(event) {
		event.preventDefault()
		const {
			usernameInput: { value: username },
			passwordInput: { value: password },
		} = event.target.elements

		if (!username) {
			setError('username is required')
		} else if (!password) {
			setError('password is required')
		} else {
			setError('')
			onSubmit({ username, password })
		}
	}

	return (
		div>
			form onSubmit={handleSubmit}>
				div>
					label htmlFor="usernameInput">Usernamelabel>
					input id="usernameInput" />
				div>
				div>
					label htmlFor="passwordInput">Passwordlabel>
					input id="passwordInput" type="password" />
				div>
				button type="submit">Submitbutton>
			form>
			{error ? div role="alert">{error}div> : null}
		div>
	)
}

export default Login

و این چیزی است که این کار انجام می دهد (در واقع کار می کند، آن را امتحان کنید):

در اینجا یک مجموعه آزمایشی است که شبیه آزمایشی است که من در طول سال ها دیده ام.

// __tests__/login.js
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'

import Login from '../login'

describe('Login', () => {
	let utils,
		handleSubmit,
		user,
		changeUsernameInput,
		changePasswordInput,
		clickSubmit

	beforeEach(() => {
		handleSubmit = jest.fn()
		user = { username: 'michelle', password: 'smith' }
		utils = render(Login onSubmit={handleSubmit} />)
		changeUsernameInput = (value) =>
			userEvent.type(utils.getByLabelText(/username/i), value)
		changePasswordInput = (value) =>
			userEvent.type(utils.getByLabelText(/password/i), value)
		clickSubmit = () => userEvent.click(utils.getByText(/submit/i))
	})

	describe('when username and password is provided', () => {
		beforeEach(() => {
			changeUsernameInput(user.username)
			changePasswordInput(user.password)
		})

		describe('when the submit button is clicked', () => {
			beforeEach(() => {
				clickSubmit()
			})

			it('should call onSubmit with the username and password', () => {
				expect(handleSubmit).toHaveBeenCalledTimes(1)
				expect(handleSubmit).toHaveBeenCalledWith(user)
			})
		})
	})

	describe('when the password is not provided', () => {
		beforeEach(() => {
			changeUsernameInput(user.username)
		})

		describe('when the submit button is clicked', () => {
			let errorMessage
			beforeEach(() => {
				clickSubmit()
				errorMessage = utils.getByRole('alert')
			})

			it('should show an error message', () => {
				expect(errorMessage).toHaveTextContent(/password is required/i)
			})
		})
	})

	describe('when the username is not provided', () => {
		beforeEach(() => {
			changePasswordInput(user.password)
		})

		describe('when the submit button is clicked', () => {
			let errorMessage
			beforeEach(() => {
				clickSubmit()
				errorMessage = utils.getByRole('alert')
			})

			it('should show an error message', () => {
				expect(errorMessage).toHaveTextContent(/username is required/i)
			})
		})
	})
})

این باید به ما اطمینان 100٪ بدهد که این مؤلفه کار می کند و ادامه خواهد داشت
مطابق طراحی کار کند. و این کار را می کند. اما اینجا چیزهایی است که من دوست ندارم
این تست:

من احساس می کنم آب و برق آن را دوست دارد changeUsernameInput و clickSubmit می تواند باشد
خوب است، اما تست ها به اندازه کافی ساده هستند که می توان این کد را به جای آن کپی کرد
کد تست ما را کمی ساده کنید. این فقط انتزاع تابع است
واقعاً استفاده زیادی از این مجموعه آزمایشی کوچک و
ما هزینه تعمیر و نگهداری را متحمل می‌شویم، بنابراین آنها باید به دنبال پرونده باشند تا کجا را ببینند
این توابع تعریف شده است.

تست های بالا با Jest API نوشته شده اند، اما شما این کار را خواهید کرد
API های مشابه را در تمام چارچوب های اصلی جاوا اسکریپت پیدا کنید. من دارم به طور خاص صحبت می کنم
در مورد describe که برای تست های گروهی استفاده می شود، beforeEach برای مشترک
تنظیم / اقدامات و it برای اظهارات واقعی

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

چه چیزی آن را اینقدر پیچیده می کند؟ این بیت را برای مثال در نظر بگیرید:

it('should call onSubmit with the username and password', () => {
	expect(handleSubmit).toHaveBeenCalledTimes(1)
	expect(handleSubmit).toHaveBeenCalledWith(user)
})

کجاست handleSubmit آیا از آن می آید و ارزش آن چیست؟ کجاست user در حال آمدن است
از؟ چیست؟ او ارزش؟ اوه، مطمئناً، می توانید به جایی که تعریف شده است بروید:

describe('Login', () => {
	let utils,
		handleSubmit,
		user,
		changeUsernameInput,
		changePasswordInput,
		clickSubmit
	// ...
})

اما بعد تو بیش از حد باید بدانم کجا اختصاص داده شده است:

beforeEach(() => {
	handleSubmit = jest.fn()
	user = { username: 'michelle', password: 'smith' }
	// ...
})

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

می توانید استدلال کنید که تخصیص مجدد متغیر “ضد الگو” است و باید باشد
اجتناب کنید و من با شما موافقم، اما قوانین پرزهای بیشتری را به بسته خود اضافه کنید
احتمالاً قوانین پرزبانی در حال حاضر راه حل خوبی نیست. چی
اگر راهی برای به اشتراک گذاشتن این تنظیمات رایج بدون دردسر وجود داشت
اصلاً تخصیص مجدد متغیر؟

برای این مولفه ساده، من فکر می کنم بهترین راه حل حذف این مقدار است
تا حد امکان انتزاعی این را بررسی کنید:

// __tests__/login.js
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'

import Login from '../login'

test('calls onSubmit with the username and password when submit is clicked', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText, getByText } = render(
		Login onSubmit={handleSubmit} />,
	)
	const user = { username: 'michelle', password: 'smith' }

	userEvent.type(getByLabelText(/username/i), user.username)
	userEvent.type(getByLabelText(/password/i), user.password)
	userEvent.click(getByText(/submit/i))

	expect(handleSubmit).toHaveBeenCalledTimes(1)
	expect(handleSubmit).toHaveBeenCalledWith(user)
})

test('shows an error message when submit is clicked and no username is provided', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText, getByText, getByRole } = render(
		Login onSubmit={handleSubmit} />,
	)

	userEvent.type(getByLabelText(/password/i), 'anything')
	userEvent.click(getByText(/submit/i))

	const errorMessage = getByRole('alert')
	expect(errorMessage).toHaveTextContent(/username is required/i)
	expect(handleSubmit).not.toHaveBeenCalled()
})

test('shows an error message when submit is clicked and no password is provided', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText, getByText, getByRole } = render(
		Login onSubmit={handleSubmit} />,
	)

	userEvent.type(getByLabelText(/username/i), 'anything')
	userEvent.click(getByText(/submit/i))

	const errorMessage = getByRole('alert')
	expect(errorMessage).toHaveTextContent(/password is required/i)
	expect(handleSubmit).not.toHaveBeenCalled()
})

توجه: test نام مستعار است it و من فقط ترجیح می دهم استفاده کنم test وقتی من نیستم
تو در تو a describe.

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

همچنین توجه داشته باشید که ما همه چیز را در a قرار نمی دهیم describe مسدود شده است زیرا
واقعا لازم نیست همه چیز در فایل به وضوح تست می شود login
جزء و شامل حتی یک سطح از تودرتو بی معنی است.

اصل AHA بیان می کند که شما باید:

تکرار را بر انتزاع اشتباه ترجیح دهید و ابتدا برای تغییر بهینه سازی کنید.

برای مؤلفه ورود ساده ما در اینجا، احتمالاً آزمایش را همانطور که هست رها می کنم، اما
بیایید تصور کنیم که کمی پیچیده تر است و ما شروع به دیدن برخی از آنها کرده ایم
مشکلات تکراری کد و ما می خواهیم آن را کاهش دهیم. باید دست دراز کنیم
beforeEach برای آن؟ منظورم این است که برای همین است؟

خوب، می‌توانستیم، اما پس از آن باید نگران متغیر قابل تغییر باشیم
دوباره وظایف و مایلیم از آن اجتناب کنیم. چگونه می توانیم کد را به اشتراک بگذاریم
بین تست های ما؟ آها! ما می توانیم از توابع استفاده کنیم!

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

import Login from '../login'

// here we have a bunch of setup functions that compose together for our test cases
// I only recommend doing this when you have a lot of tests that do the same thing.
// I'm including it here only as an example. These tests don't necessitate this
// much abstraction. Read more: https://kcd.im/aha-testing
function setup() {
	const handleSubmit = jest.fn()
	const utils = render(Login onSubmit={handleSubmit} />)
	const user = { username: 'michelle', password: 'smith' }
	const changeUsernameInput = (value) =>
		userEvent.type(utils.getByLabelText(/username/i), value)
	const changePasswordInput = (value) =>
		userEvent.type(utils.getByLabelText(/password/i), value)
	const clickSubmit = () => userEvent.click(utils.getByText(/submit/i))
	return {
		...utils,
		handleSubmit,
		user,
		changeUsernameInput,
		changePasswordInput,
		clickSubmit,
	}
}

function setupSuccessCase() {
	const utils = setup()
	utils.changeUsernameInput(utils.user.username)
	utils.changePasswordInput(utils.user.password)
	utils.clickSubmit()
	return utils
}

function setupWithNoPassword() {
	const utils = setup()
	utils.changeUsernameInput(utils.user.username)
	utils.clickSubmit()
	const errorMessage = utils.getByRole('alert')
	return { ...utils, errorMessage }
}

function setupWithNoUsername() {
	const utils = setup()
	utils.changePasswordInput(utils.user.password)
	utils.clickSubmit()
	const errorMessage = utils.getByRole('alert')
	return { ...utils, errorMessage }
}

test('calls onSubmit with the username and password', () => {
	const { handleSubmit, user } = setupSuccessCase()
	expect(handleSubmit).toHaveBeenCalledTimes(1)
	expect(handleSubmit).toHaveBeenCalledWith(user)
})

test('shows an error message when submit is clicked and no username is provided', () => {
	const { handleSubmit, errorMessage } = setupWithNoUsername()
	expect(errorMessage).toHaveTextContent(/username is required/i)
	expect(handleSubmit).not.toHaveBeenCalled()
})

test('shows an error message when password is not provided', () => {
	const { handleSubmit, errorMessage } = setupWithNoPassword()
	expect(errorMessage).toHaveTextContent(/password is required/i)
	expect(handleSubmit).not.toHaveBeenCalled()
})

اکنون می توانیم ده ها تست داشته باشیم که از این تست های ساده استفاده می کنند setup توابع وهمچنین توجه کنید که آنها می توانند با هم ترکیب شوند تا رفتار مشابهی را به ما ارائه دهند
تو در تو beforeEach اگر منطقی باشد قبلاً داشتیم. اما ما از داشتن متغیرهای متغیری که باید مراقب آنها باشیم تا آنها را در خود ردیابی کنیم اجتناب می کنیم
ذهن

شما می توانید با آزمایش در مورد مزایای AHA ها بیشتر بدانید
تست AHA

را describe تابع برای گروه بندی تست های مرتبط با هم طراحی شده است و می تواند
یک راه خوب برای جداسازی بصری تست های مختلف، به خصوص زمانی که
فایل تست بزرگ می شود. اما وقتی فایل تست بزرگ می شود دوست ندارم. بنابراین در عوض
گروه بندی آزمون ها توسط describe بلوک ها، آنها را بر اساس فایل گروه بندی کنید. پس اگر وجود داشته باشد
گروه بندی منطقی تست های مختلف را برای همان “واحد” کد، جدا خواهم کرد
با قرار دادن آنها در فایل های کاملا متفاوت. و اگر کدی هست
که واقعا باید بین آنها به اشتراک گذاشته شود، سپس من ایجاد خواهم کرد
__tests__/helpers/login.js فایلی که حاوی کد مشترک است.

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

این پست وبلاگ حمله ای به برنامه های کاربردی مانند نیست beforeEach/afterEach/و غیره
این بیشتر یک احتیاط و توجه به متغیرهای متغیر در تست ها است
انتزاعات شما

برای تمیز کردن، گاهی اوقات شما در موقعیتی گیر می کنید که چیزی که هستید
آزمایش تغییراتی را در محیط جهانی ایجاد می کند و باید پاکسازی کنید
بعد از او اگر سعی می کنید این کد را در تست خود قرار دهید، تست کنید
شکست باعث می شود پاکسازی شما کار نکند، که می تواند به دیگران منجر شود
تست ها با شکست مواجه می شوند، که در نهایت باعث ایجاد خطاهای زیادی می شود، که انجام آن سخت تر است
اشکال زدایی

توجه: این مثال قبلا نوشته شده است @testing-library/react@9 که او انجام داد
تمیز کردن خودکار اما این مفهوم هنوز پابرجاست و من نمی خواستم بازنویسی کنم
مثال 😅

به عنوان مثال، React Testing Library کامپوننت شما را در سند وارد می کند.
و اگر بعد از هر آزمایش پاکسازی نکنید، ممکن است آزمایشات شما با شکست مواجه شود
خودت:

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

import Login from '../login'

test('example 1', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	userEvent.type(getByLabelText(/password/i), 'ilovetwix')
	// more test here
})

test('example 2', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	// 💣 this will blow up because the `getByLabelText` is actually querying the
	// entire document, and because we didn't cleanup after the previous test
	// we'll get an error indicating that RTL found more than one field with the
	// label "username"
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	// more test here
})

رفع این مشکل بسیار ساده است، شما باید اجرا کنید cleanup روش از
@testing-library/react بعد از هر آزمون

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

import Login from '../login'

test('example 1', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	userEvent.type(getByLabelText(/password/i), 'ilovetwix')
	// more test here
	cleanup()
})

test('example 2', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	// more test here
	cleanup()
})

با این حال، اگر شما نکن استفاده کنید afterEach برای انجام این کار سپس اگر تست شما شکست خورد
پاکسازی شروع نمی شود، مانند این:

test('example 1', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	// 💣 the following typo will result in a error thrown:
	//   "no field with the label matching passssword"
	userEvent.type(getByLabelText(/passssword/i), 'ilovetwix')
	// more test here
	cleanup()
})

بنابراین، در cleanup تابع در “مثال 1” پس از آن اجرا نخواهد شد
“مثال 2” به درستی کار نمی کند، بنابراین به جای اینکه فقط 1 تست شکست بخورد، همه تست ها با شکست مواجه می شوند و این امر اشکال زدایی را بسیار دشوارتر می کند.

بنابراین شما باید به جای آن استفاده کنید afterEach و این تضمین می کند که حتی اگر شما
تست ناموفق بود، می توانید پاک کنید:

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

import Login from '../login'

afterEach(() => cleanup())

test('example 1', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	userEvent.type(getByLabelText(/password/i), 'ilovetwix')
	// more test here
})

test('example 2', () => {
	const handleSubmit = jest.fn()
	const { getByLabelText } = render(Login onSubmit={handleSubmit} />)
	userEvent.type(getByLabelText(/username/i), 'kentcdodds')
	// more test here
})

حتی بهتر، با کتابخانه تست React،
cleanup
به طور پیش فرض پس از هر تست به طور خودکار فراخوانی می شود. بیشتر بدانید در
اسناد

همچنین، گاهی اوقات قطعا موارد استفاده خوبی برای آن وجود دارد before*، اما
آنها معمولاً با تمیز کردن ترکیب می شوند که در آن ضروری است after*.لایک
راه اندازی و توقف سرور:

let server
beforeAll(async () => {
	server = await startServer()
})
afterAll(() => server.close())

واقعا هیچ راه قابل اعتماد دیگری برای انجام این کار وجود ندارد. یکی دیگر از موارد استفاده من می توانم
فکر می کنم از این قلاب ها برای آزمایش استفاده کردم console.error تماس می گیرد:

beforeAll(() => {
	jest.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
	console.error.mockClear()
})

afterAll(() => {
	console.error.mockRestore()
})

بنابراین قطعا موارد استفاده برای این نوع قلاب ها وجود دارد. من فقط این کار را نمی کنم
آنها را به عنوان مکانیزم استفاده مجدد از کد توصیه کنید. ما ویژگی هایی برای آن داریم.

امیدوارم این به روشن شدن منظورم در آن توییت کمک کند:

کنت اس دادز 🐨
@kentcdodds

این مدل:

test(‘هر چه که باشد، () => {
const foo = چیزی ()
// از foo استفاده کنید
})

یک بستر آزمایشی بسیار ساده تر از موارد زیر ایجاد می کند:

برویم
BeforeEach(() => {
foo = چیزی ()
})

test(‘هر چه که باشد، () => {
// از foo استفاده کنید
})

از متغیرهای قابل تغییر اجتناب کنید. درک تست های شما آسان تر خواهد بود

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

P.S. اگر می خواهید با مثال ها بازی کنید،
اینجا یک جعبه شنی کد است


Source link

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

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

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