تست های سخت تر، بهتر، سریع تر، قوی تر با وسایل

https://www.youtube.com/watch?v=gAjR4_CbPpQ
خوب پس من واقعاً می خواستم به دفت پانک اشاره کنم. با انتشار مجدد محدود آنها در دسامبر 2024 کشف و غربالگری از Interstella 5555، دوتایی الکترونیکی فرانسوی در ذهن من بوده است. البته این مرا نوستالژیک و کمی غمگین کرده است، آرزوی روزهایی که ساده تر به نظر می رسید.
بیکار بودن آسان نیست و در طول تعطیلات افسرده شده اید، پس حدس بزنید چیست؟ در اینجا یک آموزش برای شما آورده شده است که احتمالاً بدترین سال زندگی من را مشخص کنید! (از جمله سالی که به سرطان مبتلا شدم!)
اما بحث واقعی: وسایل تست می تواند تست های خود را با کاهش افزونگی، جداسازی سناریوها و افزایش عملکرد بهبود دهید.
نحوه استفاده از این آموزش
این آموزش به گونه ای طراحی شده است که سبک های مختلف یادگیری را در خود جای دهد تا بتوانید ماجراجویی خود را انتخاب کنید:
- مستقیماً به کد بروید: کد این آموزش را می توانید در اینجا پیدا کنید. از README برای راه اندازی و اجرای آن به صورت محلی استفاده کنید.
- اما اول، چه کسی به آزمایش اهمیت می دهد؟: سعی میکنم شما را متقاعد کنم که چرا نوشتن تستها میتواند سرگرمکننده باشد و بهترین روشهایی را که باید در ذهن داشته باشید فهرست میکنم.
- وسایل تست چیست؟: مقدمهای بر تجهیزات تست در واحد تست و اینکه چگونه میتوانند به شما کمک کنند تا بهترین شیوههای تست واحد را رعایت کنید.
- کمی زمینه: اگر شما از آن دسته افرادی هستید که دوست دارید سؤالات زیادی بپرسید یا در ایجاد انتظارات آرامش پیدا می کنید، من شما را دریافت کردم، نازنین مضطرب بیش از حد! این بخش قصد دارد به شما کمک کند تا برای موفقیت آماده شوید.
- تجهیزات تست در عمل: توضیحی از کد آموزشی که در آن اثرات استفاده از انواع مختلف تجهیزات تست را بررسی خواهیم کرد تا بتوانید آن را تجربه کنید.آها!” لحظه دست اول
اما اول، چه کسی به آزمایش اهمیت می دهد؟ آیا هوش مصنوعی نمی تواند این کار را انجام دهد؟
اول از همه، من به آزمایش اهمیت دهید
ثانیاً، بله… اما مگر اینکه شما یک سادیست باشید که از صفحات حادثه در ساعت سه بامداد لذت می برد و سعی می کند کدهایی را که یک ربات نوشته است را اشکال زدایی کند، احتمالاً حداقل باید کار ربات را بررسی کنید. (و – می دانید – شرمنده نیست است چیز شما.)
میتوانید پستهای قبلی من در این مجموعه را بخوانید تا درباره چرایی مهم بودن تست بیشتر بدانید، اما به طور خلاصه:
- علاوه بر مزایای کیفی تست نرم افزار مانند جلوگیری از اشکال، کاهش هزینه های توسعه و بهبود عملکرد، قانع کننده ترین مزیت تست های نوشتن این است که ما را به مهندسان بهتری تبدیل می کند. تستهای نوشتاری ما را مجبور میکند از خود بپرسیم، “رفتار مورد انتظار این روش یا برنامه دقیقا چیست؟” وقتی نرمافزار «آزمایش دشوار» میشود، معمولاً نشانگر خوبی از بوی کد و فرصتی برای اصلاح یک روش یا بازنگری کل طراحی یک سیستم است.
- یک مزیت کمتر آشکار اما همچنان مهم برای نوشتن تست ها – به ویژه تست های واحد – وظیفه مضاعف آنها به عنوان مستندسازی سریع است. بهترین روشها برای آزمونهای واحد نام توابع طولانی و توصیفی را میطلبد. این نامهای توابع نه تنها خروجی تست پرمخاطب را خواناتر و سریعتر ارزیابی میکنند، آنها همچنین مستنداتی را برای عملکرد مورد آزمایش ارائه می دهند.
- تست های نوشتاری می تواند سرگرم کننده باشد. بله درست خواندید یک مجموعه آزمایشی که به خوبی ساخته شده باشد می تواند به اندازه خود کد برنامه مشکلی برای حل کردن مشکل باشد. و آن احساس زمانی که تمام آزمون های شما قبول می شود؟ یا زمانی که آزمایشهای شما به اجرای نرمافزار یا اجرای ویژگی کمک میکنند؟ احساس می کند خوب. آزمایشها میتواند یک برد سریع دوپامین در حرفهای باشد که میتواند مملو از جلسات اشکال زدایی نیمهشب و حملات سندروم فریبنده باشد..
بهترین شیوه های تست واحد
اکنون که من با موفقیت شما را در مورد مزایای نوشتن تست متقاعد کردم و اکنون به اندازه کافی آماده شده اید، در اینجا برخی از بهترین روش های آزمون واحد وجود دارد که باید به خاطر داشته باشید.
یک تست واحد خوب باید این باشد:
- یک ادعای خاص در یک زمان
- مستقل، منزوی و کنترل شده
- مرتبط و معنادار
- تکراری و قطعی
- اتوماتیک
- توصیفی
وسایل تست چیست؟
در زمینه نرمافزار، یک دستگاه تست (که به آن “متن تست” نیز میگویند) برای تنظیم وضعیت سیستم و دادههای ورودی مورد نیاز برای اجرای آزمایش استفاده میشود. هدف یک فیکسچر آزمایشی ایجاد محیطی است که آزمون(های) در آن اجرا خواهد شد. تجهیزات آزمایشی میتوانند با کنترل متغیرهایی مانند پایگاههای داده و مجموعه دادهها، وضعیت سیستم، سیستم عامل، فایلهای خاص و ساختگیها، به آزمونها کمک کنند تا به بهترین شیوههای آزمایش واحد ما پایبند باشند.
به طور خاص، در چارچوب واحد تست پایتون، فیکسچرهای تست توابع یا روشهایی هستند که قبل یا بعد از یک تست یا گروهی از تستها برای ایجاد یک محیط آزمایشی اجرا میشوند.
وسایل آزمون در سطح کلاس و روش
فیکسچرهای کلاس و سطح روش توسط یک نمونه TestCase ارائه میشوند و بخشی از گروه روشهای مربوط به آزمایشهای در حال اجرا هستند.
وسایل آزمون در سطح کلاس متدهایی هستند که قبل و یا بعد از آن اجرا می شوند همه روش های تست در یک نمونه TestCase. آنها به این شکل هستند:
@classmethod
def setUpClass(cls):
print("Class-level setup test fixture has been executed!")
@classmethod
def tearDownClass(cls):
print("Class-level tear down test fixture has been executed!")
یک نمونه پیاده سازی ممکن است به شکل زیر باشد:
import unittest
class ExampleTestCaseClassTestFixtures(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("Class-level setup test fixture has been executed!")
@classmethod
def tearDownClass(cls):
print("Class-level teardown test fixture has been executed!")
def test_example_equal(self):
self.assertEqual(1 + 1, 2)
def test_example_not_equal(self):
self.assertNotEqual(1 + 1, 3)
اگر بخواهید تست های بالا را اجرا کنید، خروجی چیزی شبیه به این است:
Class-level setup test fixture has been executed!
test_example_equal (tests.test_example.ExampleTestCaseClassTestFixtures.test_example_equal) ... ok
test_example_not_equal (tests.test_example.ExampleTestCaseClassTestFixtures.test_example_not_equal) ... ok
Class-level teardown test fixture has been executed!
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
💡 این مثال در مخزن و تا زمانی که در آن هستید ارائه می شود fixtures-tutorial
شعبه می توانید آن را از دایرکتوری ریشه اجرا کنید: python -m unittest tests.examples.test_test_case_fixtures_example.ExampleTestCaseClassTestFixtures -v
وسایل تست سطح روش متدهایی هستند که قبل و یا بعد از آن اجرا می شوند هر روش تست در یک نمونه TestCase. آنها به این شکل هستند:
def setUp(self):
print("Method-level setup test fixture has been executed!")
def tearDown(self):
print("Method-level teardown test fixture has been executed!")
یک نمونه پیاده سازی ممکن است به شکل زیر باشد:
class ExampleTestCaseMethodTestFixtures(unittest.TestCase):
def setUp(self):
print("Method-level setup test fixture has been executed!")
def tearDown(self):
print("Method-level teardown test fixture has been executed!")
def test_example_equal(self):
self.assertEqual(1 + 1, 2)
def test_example_not_equal(self):
self.assertNotEqual(1 + 1, 3)
اگر بخواهید تست های بالا را اجرا کنید، خروجی چیزی شبیه به این است:
test_example_equal (tests.test_example.ExampleTestCaseMethodTestFixtures.test_example_equal) ... Method-level setup test fixture has been executed!
Method-level teardown test fixture has been executed!
ok
test_example_not_equal (tests.test_example.ExampleTestCaseMethodTestFixtures.test_example_not_equal) ... Method-level setup test fixture has been executed!
Method-level teardown test fixture has been executed!
ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
توجه داشته باشید که چگونه در این خروجی پیامهای راهاندازی و حذف دو بار تکرار میشوند – یک بار برای هر یک از دو روش تست در مورد تست.
💡 این مثال در مخزن و تا زمانی که در آن هستید ارائه می شود fixtures-tutorial
شعبه می توانید آن را از دایرکتوری ریشه اجرا کنید: python -m unittest tests.examples.test_test_case_fixtures_example.ExampleTestCaseMethodTestFixtures -v
تجهیزات تست در سطح ماژول
تجهیزات تست سطح ماژول توابعی هستند که قبل و/یا بعد از آن اجرا می شوند تمام تست ها در یک ماژول اجرا می شوند. این وسایل معمولاً برای تنظیم و از بین بردن منابعی که در چندین آزمایش در یک ماژول به اشتراک گذاشته می شوند، استفاده می شوند. آنها به این شکل هستند:
def setUpModule():
print("Module-level setup test fixture has been executed!")
def tearDownModule():
print("Module-level teardown test fixture has been executed!")
یک نمونه پیاده سازی ممکن است به شکل زیر باشد:
import unittest
def setUpModule():
print("Module-level setup test fixture has been executed!")
def tearDownModule():
print("Module-level teardown test fixture has been executed!")
class ExampleTestCaseSecond(unittest.TestCase):
def test_example_equal(self):
self.assertEqual(1 + 1, 2)
def test_example_not_equal(self):
self.assertNotEqual(1 + 1, 3)
class ExampleTestCaseFirst(unittest.TestCase):
def test_example_equal(self):
self.assertEqual(1 + 1, 2)
def test_example_not_equal(self):
self.assertNotEqual(1 + 1, 3)
اگر بخواهید تست های بالا را اجرا کنید، خروجی چیزی شبیه به این است:
Module-level setup test fixture has been executed!
test_example_equal (tests.examples.test_module_fixtures_example.ExampleTestCaseFirst.test_example_equal) ... ok
test_example_not_equal (tests.examples.test_module_fixtures_example.ExampleTestCaseFirst.test_example_not_equal) ... ok
test_example_equal (tests.examples.test_module_fixtures_example.ExampleTestCaseSecond.test_example_equal) ... ok
test_example_not_equal (tests.examples.test_module_fixtures_example.ExampleTestCaseSecond.test_example_not_equal) ... ok
Module-level teardown test fixture has been executed!
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
من مطمئن هستم که شما قبلاً متوجه شده اید که این خروجی چگونه با دو مثال قبلی متفاوت است زیرا شما اینطور باهوش هستید!
💡 این مثال در مخزن و تا زمانی که در آن هستید ارائه می شود fixtures-tutorial
شعبه می توانید آن را از دایرکتوری ریشه اجرا کنید: python -m unittest tests.examples.test_module_fixtures_example -v
اکنون که درک اولیه ای از تجهیزات تست دارید، بیایید آنها را در عمل ببینیم.
برای مطالعه بیشتر در مورد تجهیزات تست در واحد تست، به مستندات اینجا مراجعه کنید.
کمی زمینه
شما می توانید کد این آموزش را در این مخزن در سایت پیدا کنید fixtures-tutorial
شاخه
Build-a-Pug یک برنامه Flask است که با یک نقطه پایانی OpenAI تماس می گیرد تا تصویری از یک پاگ را بر اساس درخواستی که از ورودی ارائه شده توسط کاربر ساخته شده است، ایجاد کند. برای این تکرار Build-a-Pug، من یک پایگاه داده SQLite برای ذخیره پاگ های ساخته شده اضافه کرده ام که می توانند از طریق See Your Grumble
صفحه (زیرا این همان چیزی است که به گروهی از پاگ ها می گویند – “غرغر کردن”!).
SQLite یک موتور پایگاه داده SQL تعبیه شده است. بر خلاف سایر پایگاه های داده SQL، SQLite یک فرآیند سرور جداگانه ندارد و مستقیماً روی فایل های دیسک معمولی می خواند و می نویسد. به خاطر این آموزش، نیازی نیست که خودتان را بیش از حد به کارکردهای داخلی SQLite درگیر کنید. تنها چیزی که باید بدانید این است که مقداردهی اولیه پایگاه داده به یک مرحله صریح اضافی نیاز دارد و پایگاه داده به صورت یک واحد ظاهر می شود. .sqlite
فایل
هشدارها و عیب یابی
از آنجایی که OpenAI دسترسی به تصاویر تولید شده را برای مدت زمان محدودی فراهم می کند، بسته به اینکه چه زمانی غرغر خود را مشاهده می کنید، برخی از تصاویر ممکن است یک خطای تأیید اعتبار امضای نامعتبر را برگردانند که به صورت تصاویر شکسته ظاهر می شوند. این به این دلیل است که تصاویر در هیچ کجا ذخیره نمی شوند و اجرای آن عملکرد خارج از محدوده این آموزش خاص است.
برای رفع این مشکل، می توانید پایگاه داده را حذف کرده و پایگاه داده جدیدی را مقداردهی کنید. با این حال، این بدان معنی است که تمام پاگ های شما برای همیشه گم می شوند.
لطفاً این برنامه را برای تولید در هر جایی مستقر نکنید
این برنامه در ابتدا به عنوان یک برنامه نمایشی اسباب بازی ایجاد شد. همانطور که در مورد آن تکرار کردم، مشخص شده است که نیاز به بازسازی دارد. بدیهی است که برای تولید در نظر گرفته نشده است، و همچنین نمونه خوبی از نحوه پیاده سازی صحیح یک پایگاه داده نیست. انجام می دهد با موفقیت نشان دهید که چگونه و چرا ابزارهای تست مفید هستند – به خصوص زمانی که شروع به اضافه کردن پیچیدگی می کنید (مانند پایگاه داده). از این برنامه به عنوان یک منبع یادگیری استفاده کنید و چه کسی می داند؟ شاید من یک آموزش در مورد refactoring انجام دهم!
نحوه استفاده از کد تست
موفق ترین یادگیری زمانی اتفاق می افتد که به آن دست یابیدآها!” تجربه من آن را به یک ترفند جادویی تشبیه می کنم: این لحظه شگفتی و لذت است که مغز شما نه تنها به طرز خوشایندی شگفت زده می شود، بلکه شیفته آن می شود. کنجکاوی بازیگوش را دعوت می کند. برای تلاش برای ایجاد مجدد آن تجربه، بخشهایی از کد آزمون را برای شما توضیح دادهام تا بعداً آن را حذف کرده و اجرا کنید تا بتوانید خودتان ببینید که چگونه تجهیزات مختلف تست بر نتایج آزمون تأثیر میگذارند.
تجهیزات تست در عمل
Build-a-Pug یک برنامه Flask است که با یک نقطه پایانی OpenAI تماس می گیرد تا تصویری از یک پاگ را بر اساس درخواستی که از ورودی ارائه شده توسط کاربر ساخته شده است، ایجاد کند. برای این تکرار Build-a-Pug، من یک پایگاه داده SQLite برای ذخیره پاگ های ساخته شده اضافه کرده ام که می توان آن را از طریق صفحه See Your Grumble بازیابی کرد (زیرا این همان چیزی است که به گروهی از پاگ ها می گویند – “grumble” !).
پیش نیازها
راه اندازی
- کلون کردن مخزن:
git clone https://github.com/liz-acosta/testing-strategies-for-python.git
- دایرکتوری را به پروژه تغییر دهید
- بررسی کنید
fixtures-tutorial
شعبه:git checkout fixtures-tutorial
- وابستگی ها را از Pipfile.lock نصب کنید:
pipenv install
- با تغییر نام، متغیرهای محیط را اضافه کنید
.env_template
به.env
… - … و جایگزینی اسرار مکان نگهدار با اسرار واقعی
- پایگاه داده SQLite را راه اندازی کنید:
pipenv run init-db
- اختیاری: پایگاه داده را حذف کنید:
pipenv run delete-db
برنامه را به صورت محلی اجرا کنید
در حالی که نیازی به اجرای برنامه برای آموزش ندارید، درک تست ها می تواند مفید باشد.
برخی از هشدارها برای این برنامه وجود دارد، برای اطلاعات بیشتر به بخش هشدارها و عیب یابی مراجعه کنید.
- برای اجرای برنامه به صورت محلی:
pipenv run start-app
- حرکت به
http://localhost:5000/
در مرورگر شما
شما باید چیزی شبیه این دریافت کنید:
از اینجا می توانید پاگ خود را بسازید:
درباره پاگ بیشتر بدانید:
یا ناله خود را بررسی کنید:
تست ها را اجرا کنید
تست ها را با وسایل تست سطح روش اجرا کنید
تست هایی که ما به آنها علاقه مندیم در آنها قرار دارند tests/unit/test_pug.py
– و به طور خاص، ما می خواهیم نگاهی به تست هایی بیندازیم که مربوط به عملیات پایگاه داده است.
محل مورد آزمایشی که نامیده می شود را پیدا کنید TestPugDBWithMethodLevelFixtures
و به آنچه که کد انجام می دهد نگاه کنید:
# A method-level test fixture that
# creates and inserts data into a sqlite database before each test in this class
def setUp(self):
"""Create a test database before each test method in this class"""
self.connection = sqlite3.connect(TEST_DATABASE_FILEPATH)
self.connection.row_factory = sqlite3.Row
test_pug_lily = Pug("Lily", "6", "San Francisco", "4:00 PM")
test_pug_lily.description = "Lily is the best pug"
test_pug_lily.image = "lily_pug.jpg"
test_pug_fiona = Pug("Fiona", "2", "San Francisco", "4:00 PM")
test_pug_fiona.description = "Fiona is the best pug"
test_pug_fiona.image = "sweet_fiona.jpg"
test_pugs = [test_pug_lily, test_pug_fiona]
with open("build_a_pug/schema.sql", "r") as f:
self.connection.executescript(f.read())
query = "INSERT INTO pug (name, age, home, puppy_dinner, description, image) VALUES (?, ?, ?, ?, ?, ?)"
for pug in test_pugs:
self.connection.cursor().execute(
query,
(
pug.name,
pug.age,
pug.home,
pug.puppy_dinner,
pug.description,
pug.image,
),
)
self.connection.commit()
print(
Fore.GREEN
+ f"Test database: {TEST_DATABASE_FILEPATH} connection created and test data inserted"
)
# A method-level test fixture that
# closes and deletes the previously created sqlite database after each test in this class
def tearDown(self):
"""Close and delete the test database after each test method in this class"""
self.connection.close()
os.remove(TEST_DATABASE_FILEPATH)
print(
Fore.RED
+ f"Test database: {TEST_DATABASE_FILEPATH} connection closed and deleted"
)
این کد از فیکسچرهای کلاس سطح متد برای موارد زیر استفاده می کند:
- یک اتصال پایگاه داده SQLite ایجاد کنید، یک جدول در پایگاه داده ایجاد کنید، چند نمونه از کلاس Pug ایجاد کنید و آنها را در پایگاه داده قرار دهید.
- اتصال پایگاه داده را ببندید و پایگاه داده را حذف کنید
💡 تعداد زیادی دیگ بخار SQLite/پایگاه داده در اینجا وجود دارد که لازم نیست نگران آنها باشید – فقط روی setUp
و tearDown
روش ها و چگونگی تاثیر آنها بر تست ها
برای راحتی شما، من خروجی چاپی روش های ثابت تست را با کد رنگی کدگذاری کرده ام.
تست ها را با: pipenv run pug-unit-tests
اگر همه چیز طبق برنامه پیش رفت، همه آزمایشها باید بگذرند و باید خروجی چاپ شده تنظیم و حذف را برای هر روش آزمایشی اجرا کنید.
تست ها را با وسایل تست سطح کلاس اجرا کنید
حالا بیایید ببینیم وقتی از وسایل سطح کلاس استفاده می کنیم چه اتفاقی می افتد.
- کلی نظر بدید
TestPugDBWithMethodLevelFixtures
کلاس - کلاس را لغو نظر کنید
TestPugDBWithClassLevelFixtures
. - تست ها را اجرا کنید:
pipenv run pug-unit-tests
آیا این شکست آزمون را گرفتید؟: AssertionError: 3 != 2
از آنجایی که پایگاه داده قبل و بعد از آن ایجاد و پاره شد تمام روش های آزمون اجرا شد، میناکاری که ما در آن اضافه کردیم test_create_pug
آزمون هنوز در پایگاه داده است و بنابراین بر نتایج آن تأثیر می گذارد test_get_grumble
تست کنید.
احتمالاً متوجه شده اید که خروجی چاپ سبز و قرمز فقط یک بار ظاهر می شود.
تست ها را با وسایل تست سطح ماژول اجرا کنید
من را تحمل کنید زیرا این یکی کمی مشکل است.
- کلی نظر بدید
TestPugDBWithClassLevelFixtures
کلاس - کلاس را لغو نظر کنید
TestPugDBWithModuleLevelFixtures
- در بالای فایل، توابع را از کامنت خارج کنید
setUpModule
وtearDownModule
- تست ها را اجرا کنید:
pipenv run pug-unit-tests
این بار با همان خطای ادعا مواجه می شویم.
به طور مشابه، خروجی چاپ شده سبز و قرمز فقط یک بار ظاهر می شود، اما به جای بسته بندی یک کلاس مورد آزمایشی خاص، کل ماژول را رزرو می کنند.
در نتیجه
امیدوارم که این آموزش نشان داده باشد که چگونه وسایل تست میتوانند با اعمال بهترین روشها مانند جداسازی موارد تست، کنترل متغیرها و بهبود عملکرد به اصلاح بیشتر تستهای شما کمک کنند.
در این مثال خاص، یک پایگاه داده آزمایشی به ما این امکان را می دهد که پایگاه داده واقعی خود را بدون آسیب رها کنیم. در حالی که ما توانست این پایگاه داده آزمایشی را در بالای ماژول آزمایشی خود راه اندازی کنید، چنین منبع بالقوه تخلیه ممکن است برای همه آزمایش های ما ضروری نباشد و می تواند نتایج آزمایش را به روش های ناخواسته تحت تأثیر قرار دهد. ما همچنین این گزینه را داریم که پایگاه داده آزمون خود را در سطح کلاس راه اندازی کنیم، اما همانطور که شاهد بودیم، این بدان معناست که روش های آزمون در آن مورد آزمایشی به یکدیگر وابسته هستند و دیگر مستقل نیستند.
از آنجایی که تستهای ما در حال حاضر نوشته شدهاند، به نظر میرسد که وسایل در سطح روش بهترین خدمات را به ما میدهند. با این حال، این می تواند با تکامل نیازهای آزمایشی ما تغییر کند و ما را مجبور کند که واقعاً آنچه را که کد مورد آزمایش واقعاً برای انجام آن قرار دارد، درونی کنیم.
تست کردن، مانند زندگی، پر از چالش است، اما همچنین مملو از فرصت هایی برای رشد، وضوح و حتی کمی سرگرمی است. با استفاده از ابزارهایی مانند وسایل تست، میتوانیم هرج و مرج را کاهش دهیم و به کدی که مینویسیم اعتماد به نفس پیدا کنیم – چیزی که در مواقعی که ممکن است برای جلوگیری از سندروم فریبنده فقط به مقدار کمی دوپامین نیاز داشته باشیم، بسیار معنادار است. چه در نیمه شب در حال رفع اشکال هستید یا فقط سعی می کنید روز را پشت سر بگذارید، به یاد داشته باشید که هر قدم کوچک رو به جلو مهم است.
آیا از وسایل تست استفاده کرده اید؟ آنها چگونه به آزمایشات شما کمک کرده یا مانع انجام آن شده اند؟
اگر از این آموزش لذت بردید، لطفاً برای من یک قهوه بخرید.
منابع بیشتر در مورد وسایل تست