testing یا تست نویسی یکی از مباحث بسیار مهم در برنامه نویسی است و اختصاصی به پایتون ندارد. ما در طول این دوره کدهای ساده ای را نوشته ایم اما در دنیای واقعی صدها هزاران یا میلیون ها خط کد برای برنامه های بزرگ نوشته می شود. به طور مثال در یک شرکت بزرگ که صدها کارمند و هزاران فایل پایتون دارد، کارمندان در زمان های مختلفی روی این فایل ها کار می کنند. تمام این تغییرات روی کدها بعضا همزمان و بعضا به ترتیب اتفاق می افتد و در بین این همه شلوغی، تصحیح یک کد سخت تر و سخت تر می شود. به طور مثال اگر شما قسمتی از یک کد را ویرایش کنید، ممکن است باعث ایجاد یک باگ در قسمت دیگری از کدها و فایل ها بشوید و پیدا کردن این خطا کار بسیار دشواری خواهد بود.
اینجاست که testing یا «تست نویسی» وارد عرصه عمل شده و مشکلات مربوط به بروز خطاهای عجیب را حل می کند. تست نویسی فرآیندی است که ما در آن به نوشتن توابع جزئی و هدف دار می پردازیم. این توابع در نهایت روی کد ما اجرا شده و رفتار آن را تست می کنند.
من پوشه جدیدی به نام testing را ایجاد می کنم که درون خودش دو فایل به نام های main.py و testing.py وجود داشته باشد. معمولا اینطور است که به ازای هر ماژول (فایل) پایتون یک فایل تست نیز خواهیم داشت. ابتدا به فایل main.py رفته و تابع ساده زیر را درون آن می نویسیم:
def do_stuff(num): return num + 5
این تابع بسیار ساده عددی را دریافت کرده و آن را با ۵ جمع می زند. در مرحله بعدی به فایل testing.py رفته و به شکل زیر عمل می کنیم:
import unittest import main
unittest یکی از ماژول های پیش ساخته در پایتون است که به ما اجازه نوشتن تست را می دهد. main نیز همان فایل ما است که حاوی تابع do_stuff می باشد. برای نوشتن تست ها باید ابتدا یک کلاس را تعریف کنیم و سپس از ماژول unittest تابع TestCase را به ارث می بریم:
import unittest import main class TestMain(unittest.TestCase):
در مرحله بعدی باید متدی را برای این کلاس بنویسیم که مسئول تست کردن تابع ما است:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 15)
من نام متد خود را test_do_stuff گذاشته ام و بر اساس توضیحات ارائه شده در قسمت برنامه نویسی شیء گرا در پایتون می دانیم که باید self را به آن بدهیم. من در این متد متغیری به نام test_param را تعریف کرده ام که مقدار ۱۰ را دارد، سپس متد خودمان، یعنی do_stuff از فایل main، را با این مقدار صدا زده ام. در نهایت به دلیل ارث بری از TestCase، متدی به نام assertEqual در دسترس ما است که کارش تست کردن و بررسی برابری در دو مقدار می باشد. تابع do_stuff عددی را می گرفت و آن را با ۵ جمع می زد بنابراین می دانیم که اگر ۱۰ را به آن پاس بدهیم باید ۱۵ را دریافت کنیم. با این حساب result (نتیجه برگردانده شده توسط تابع do_stuff) و عدد ۱۵ را به assertEqual داده ایم. کار این تابع این است که برابر بودن این دو مقدار را بررسی کند. در نظر داشته باشید که ما می توانیم چندین تست را در قالب چندین متد درون این کلاس داشته باشیم اما فعلا فقط به یک تست نیاز داریم. در مرحله آخر باید این تست را صدا بزنیم:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 15) unittest.main()
متد main باعث اجرای تمام تست های موجود در کلاس TestMain می شود. با اجرای کد بالا نتیجه زیر را می گیریم:
g.py" . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
اجرای تست زیر صفر ثانیه بوده است چرا که اجرای تابع do_stuff بسیار بسیار سریع است بنابراین زمان اجرای آن به شکل 0.000 برگردانده شده است. مهم ترین قسمت این گزارش همان OK است که نشان از صحت تست و تابع ما دارد. حالا بیایید این تست را کمی تغییر بدهیم تا از عمد نتیجه ای اشتباه تولید شود:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 10) unittest.main()
ما می دانیم که نتیجه اجرای do_stuff برابر ۱۰ نخواهد بود و این تست به مشکل برمی خورد. با اجرای کد بالا گزارش زیر را دریافت می کنید:
g.py" F ====================================================================== FAIL: test_do_stuff (__main__.TestMain) ---------------------------------------------------------------------- Traceback (most recent call last): File "/mnt/Development/Roxo Academy/Python/training3/testing.py", line 9, in test_do_stuff self.assertEqual(result, 10) AssertionError: 15 != 10 ---------------------------------------------------------------------- Ran 1 test in 0.004s FAILED (failures=1)
همانطور که می بینید در ابتدا نام تستی که شکست خورده است نمایش داده می شود (test_do_stuff) سپس متن خطا نمایش داده شده است و در قسمت AssertionError می بینیم که ۱۵ برابر با ۱۰ نیست. این همان خطای ما بوده است. در نهایت نیز failures=1 را داریم که تعداد شکست های این تست را مشخص می کند. بیایید یک تست دیگر را اضافه کنیم:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 15) def test_do_stuff2(self): test_param = 'A simple string' result = main.do_stuff(test_param) self.assertTrue(result) unittest.main()
تست جدید ما به جای عدد رشته ای را به تابع do_stuff پاس می دهد که طبیعتا باعث خطای TypeError می شود. من در این بخش از متد assertTrue استفاده کرده ام که بررسی می کند مقدار پاس داده شده به آن true باشد. با اجرای کد بالا نتیجه زیر را می گیریم:
g.py" .E ====================================================================== ERROR: test_do_stuff2 (__main__.TestMain) ---------------------------------------------------------------------- Traceback (most recent call last): File "/mnt/Development/Roxo Academy/Python/training3/testing.py", line 13, in test_do_stuff2 result = main.do_stuff(test_param) File "/mnt/Development/Roxo Academy/Python/training3/main.py", line 2, in do_stuff return num + 5 TypeError: can only concatenate str (not "int") to str ---------------------------------------------------------------------- Ran 2 tests in 0.005s FAILED (errors=1)
همانطور که می بینید TypeError خود را در این بخش دریافت کرده ایم. احتمالا می گویید فایده نوشتن این تست ها چیست؟ با انجام این کار می توانیم خطاهای احتمالی را تشخیص بدهیم و از بروز آن ها در برنامه واقعی جلوگیری کنیم. به طور مثال در حال حاضر باید به main.py برگشته و تابع را تصحیح کنیم:
def do_stuff(num): try: return num + 5 except TypeError as err: return err
با انجام این کار خطاها را دریافت کرده و برمی گردانیم بنابراین تست شکست نمی خورد. در واقع اگر تست را دوباره اجرا کنید نتیجه زیر را می گیرید:
g.py" .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
به همین راحتی دو تست بسیار ساده را نوشتیم اما در نظر داشته باشید که assertTrue کردن result کار جالبی نیست. چرا؟ به دلیل اینکه اگر تابع ما طوری نوشته می شد که مقدار برگردانده شده اش صفر می شد، دیگر true نمی بود! همچنین برخی اوقات واقعا می خواهیم نتیجه و حالت اشتباه را بررسی کنیم. در این حالت می توان به شکل زیر عمل کرد:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 15) def test_do_stuff2(self): test_param = 'A simple string' result = main.do_stuff(test_param) self.assertTrue(isinstance(result, TypeError)) unittest.main()
من می خواهم مطمئن شوم که با پاس دادن یک رشته، حتما یک خطا را دریافت می کنیم بنابراین از تابع isinstance استفاده کرده ام که دو آرگومان را گرفته و مشخص می کند آیا اولی، نمونه ای از دومی است یا خیر. اگر اینطور باشد این تابع مقدار true را برمی گرداند و assertTrue نیز برابر true خواهد بود اما در غیر این صورت تست شکست می خورد. با اجرای کد بالا نتیجه زیر را دریافت می کنیم:
g.py" .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
در نسخه ۳.۲ زبان پایتون متد جدیدی به نام assertIsInstance نیز معرفی شده است که کار ما را ساده تر کرده و به ما اجازه می دهد تست دوم را به شکل زیر بنویسیم:
def test_do_stuff2(self): test_param = 'A simple string' result = main.do_stuff(test_param) self.assertIsInstance(result, TypeError)
با این کار باز هم نتیجه قبلی را می گیریم. در نظر داشته باشید که اگر در خود تابع do_stuff قسمتی برای گرفتن خطاها (except) نداشته باشیم این تست ها شکست می خورند. چرا؟ هدف ما از نوشتن تست این است که از متوقف شدن برنامه جلوگیری کرده و توابع بدون خطایی داشته باشیم. اگر تابع ما دچار خطا شده و اسکریپت را متوقف کند، هدف ما شکست خورده است. تست بالا با موفقیت اجرا می شود چرا که ما در do_stuff خطا را دریافت کرده و نهایتا آن را برگردانده ایم.
من به فایل main.py رفته و این بار تابع خودمان را کمی تغییر می دهم:
def do_stuff(num): try: return int(num) + 5 except TypeError as err: return err
این بار num را به ()int داده ام تا مطمئن شویم که اگر کاربر به جای عددی مثل ۱۰، رشته "۱۰" را ارسال کند باز هم تابع ما آن را به عدد تبدیل کرده و به راحتی جمع بزند. در حال حاضر اگر دوباره تست را اجرا کنیم، گزارش زیر را دریافت خواهیم کرد:
g.py" .E ====================================================================== ERROR: test_do_stuff2 (__main__.TestMain) ---------------------------------------------------------------------- Traceback (most recent call last): File "/mnt/Development/Roxo Academy/Python/training3/testing.py", line 13, in test_do_stuff2 result = main.do_stuff(test_param) File "/mnt/Development/Roxo Academy/Python/training3/main.py", line 3, in do_stuff return int(num) + 5 ValueError: invalid literal for int() with base 10: 'A simple string' ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (errors=1)
چرا دوباره خطا گرفته ایم؟ به دلیل اینکه این بار خطای ما از نوع ValueError است اما ما فقط TypeError را دریافت کرده بودیم. برای حل این مشکل می توانیم باز هم به تابع do_stuff برویم و یک قسمت جدید برای TypeError اضافه کنیم:
def do_stuff(num): try: return int(num) + 5 except TypeError as err: return err except ValueError as err: return err
مشکل این روش از نظر من این است که در آینده خطاهای مختلفی خواهیم داشت بنابراین بهتر است تمام خطاها را یکجا دریافت کنیم:
def do_stuff(num): try: return int(num) + 5 except: return "Something is wrong"
سپس به فایل test.py برگشته و این بار از روش دیگری برای دریافت خطا استفاده می کنیم:
import unittest import main class TestMain(unittest.TestCase): def test_do_stuff(self): test_param = 10 result = main.do_stuff(test_param) self.assertEqual(result, 15) def test_do_stuff2(self): test_param = 'A simple string' result = main.do_stuff(test_param) self.assertRaises((ValueError, TypeError), result) unittest.main()
assertRaises بررسی وجود خطاهای مختلف را بر عهده دارد. آرگومان اول همیشه باید یک tuple از انواع خطاهای مختلف یا فقط یک نوع خطا باشد. آرگومان دوم نیز مقدار مورد نظر ما است. با اجرای کد بالا نتیجه زیر را می گیریم:
g.py" .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
چرا؟ به دلیل اینکه در این تست خطاهای ValueError و TypeError را کنترل کرده ایم بنابراین تست موفقیت آمیز خواهد بود. ما در این مقاله مبحث تست نویسی را به صورت خلاصه بررسی کردیم. در صورتی که به این بحث علاقه دارید پیشنهاد می کنم documentation رسمی زبان پایتون در این باره را مطالعه نمایید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.