پنج باگی را که آخر از همه در محیط واقعی دیدی کنار هم بگذار. کدامیک از تستهایت هر کدام از آنها را میگرفت؟ اگر جواب «هیچکدام» است، عدد پوشش تستت هر چقدر هم بالا باشد، چیز زیادی دربارهی محافظت نمیگوید.
پوشش میگوید کدام خطوط هنگام اجرای تستها اجرا شدهاند، نه اینکه رفتار درست سنجیده شده باشد. و تست آسان معمولا روی کد آسان نوشته میشود: تابعهای کوچک و خالص. باگها اما در محل برخورد چیزها مینشینند: تراکنش، دو اجرای همزمان، ورودی مرزی. تست نویسی خوب از همین فاصله شروع میشود.
اولویتها، از گرانترین خطا
| اولویت | چه چیزی | چرا |
|---|---|---|
| اول | مسیرهای پولی و امنیتی | گرانترین خطا، و اغلب بیصداترین |
| دوم | باگهایی که یک بار اتفاق افتادهاند | ثابت شده همانجا میشکند |
| سوم | منطق خالص پیچیده | ارزانترین تست با بیشترین بازده |
| چهارم | قرارداد میان ماژولها | جایی که تغییر یکی دیگری را میشکند |
| ننویس | کد چسب و پیکربندی | تستش فقط خود کد را تکرار میکند |
ردیف دوم مهمترین است: هر باگی که یک بار در محیط واقعی دیدی یک تست بدهکار است. و ردیف آخر را هم جدی بگیر. تستی که فقط تکرار کد است محافظت نمیدهد ولی هزینهی نگهداری دارد؛ با هر تغییر بیضرر قرمز میشود، و تو کمکم یاد میگیری قرمز را نادیده بگیری. از آن روز، قرمز واقعی هم دیده نمیشود. چنین تستی را یا طوری بازنویسی کن که رفتار را بسنجد نه پیادهسازی را، یا حذفش کن.
تفاوت این دو در عمل ساده است. تستی که به پیادهسازی چسبیده میپرسد «این تابع آن تابع دیگر را با این آرگومانها صدا زد؟» و با هر جابهجایی بیخطر در کد قرمز میشود. تستی که رفتار را میسنجد میپرسد «با این ورودی، این خروجی بیرون آمد؟» و تا وقتی رفتار عوض نشده سبز میماند. دومی است که اجازه میدهد بیترس بازسازی کنی.
تابعی که نمیشد تستش کرد
مثالی که دنبال میکنیم یک تابع فرضی اما واقعنما است: هر بار که کاربری ابزاری را دانلود میکند، کاربر از پایگاه داده خوانده میشود، یک ردیف به فهرست دانلودهایش اضافه میشود، فهرست ذخیره میشود، و اگر تعداد دانلودها به دو رسید یک رویداد «نقطهی عطف» فعال میشود.
این تابع یک باگ بیصدا هم داشت. فهرست دانلودها همان آرایهی قبلی بود که درجا به آن چیزی اضافه میشد و همان آرایه دوباره برای ذخیره فرستاده میشد. لایهی داده، که تغییر را با مقایسهی مرجع تشخیص میدهد، هیچ تغییری نمیدید و چیزی ذخیره نمیشد. هیچ خطایی هم پرتاب نمیشد.
و نمیشد برایش تست نوشت، چون پایگاه داده و تنظیمات محیط و منطق در هم پیچیده بودند. برای اجرای هر تست باید یک پایگاه دادهی واقعی بالا میآمد.
جدا کردن تصمیم از اثر
قدم اول یک تفکیک ساده است. در این تابع چه چیزهایی تصمیماند و چه چیزهایی اثر؟
- تصمیمها: فهرست تازه چه شکلی دارد؛ آیا از آستانه رد شدهایم.
- اثرها: خواندن از پایگاه داده، نوشتن در آن، فعال کردن رویداد.
فقط تصمیمها ارزش تست واحد دارند. اثرها را باید در محیط واقعی دید.
قدم دوم این است که تصمیمها به دو تابع خالص منتقل شوند؛ تابعهایی که جز آرگومانهایشان ورودیای ندارند و جز مقدار برگشتی خروجیای. اولی فهرست قبلی و شناسهی ابزار را میگیرد و یک آرایهی تازه برمیگرداند، و اگر فهرست قبلی آرایه نبود آن را خالی فرض میکند. دومی فهرست را میگیرد و میگوید آستانه رد شده یا نه.
قدم سوم: تابع اصلی فقط هماهنگکننده میشود. کاربر را میخواند، فهرست تازه را از تابع اول میگیرد، همان را ذخیره میکند و با تابع دوم تصمیم میگیرد رویداد را فعال کند یا نه. چون حالا آرایهی تازه با مرجع تازه ذخیره میشود، باگ ذخیره نشدن هم در همین قدم رفع شد. تابع اصلی از هشت خط درهم به پنج خط خوانا رسید.
سه تستی که آن باگ را برای همیشه میگیرند
حالا میشود تستهایی نوشت که محافظت میکنند، نه تستهایی که کد را تکرار کنند:
- تابع افزودن یک مرجع تازه برمیگرداند، نه همان آرایهی ورودی را. این تست مستقیم همان باگ را میگیرد، و نامش باید بگوید چرا وجود دارد: وگرنه لایهی داده تغییر را نمیبیند.
- ورودی را دستکاری نمیکند؛ آرایهای که با یک عضو داده شد، بعد از صدا زدن هنوز یک عضو دارد.
- مقدار نامعتبر، چه null و چه تعریفنشده، مثل آرایهی خالی رفتار میکند.
برای این کار وابستگی تازه لازم نیست. Node از نسخهی هجده یک تسترانر داخلی دارد که با یک فرمان اجرا میشود. این یک تصمیم سنجیده است، نه کمبود: سطح حمله را بزرگ نمیکند، نصبی نمیخواهد که در شبکهی محدود گیر کند، و با خود Node بهروز میشود.
یک قید صادقانه هم بماند. این تفکیک مسیر پایگاه داده را تست نمیکند. اگر ذخیرهسازی شکست بخورد، هیچکدام از این سه تست آن را نمیگیرند. برای آن تست یکپارچگی یا پایش محیط واقعی لازم است، و باید بدانی که اینجا پوشش داده نشده.
تست توصیفی، پیش از دست زدن به کد قدیمی
وقتی میخواهی کد قدیمی و بیتست را بازسازی کنی، اولین تست نباید بگوید رفتار درست چیست. باید بگوید رفتار فعلی چیست، حتی اگر باگ داشته باشد. اگر تستی بنویسی که رفتار مطلوب را میسنجد، از روز اول قرمز است و دیگر نمیفهمی بازسازی چیزی را شکسته یا نه.
روشش این است: تابعی بیش از سی خط با چند شاخه انتخاب کن. بدون تغییر کد، ورودیهای متنوع بده و خروجیها را یادداشت کن. تستی بنویس که همان خروجیها را تایید کند، حتی آنهایی که به نظرت غلطاند، و کنار هر کدام از اینها کامنت بگذار که این رفتار فعلی است و درستیاش بررسی نشده. حالا بازسازی کن؛ تستها باید سبز بمانند. رفتارهای مشکوک را بعدا و در یک کامیت جدا بررسی کن.
جدا کردن کامیت بازسازی از کامیت اصلاح سادهترین کاری است که بیشترین وقت را نجات میدهد. بازسازی باید رفتار را حفظ کند؛ اصلاح عمدا آن را عوض میکند. اگر هر دو در یک کامیت باشند و چیزی بشکند، نمیدانی از کدام بوده و باید کل کار را دوباره بازبینی کنی.
هر باگ واقعی، یک تست رگرسیون
برای باگی که در محیط واقعی پیدا کردهای تستی بنویس که آن را بازتولید کند. روی کد پیش از اصلاح اجرایش کن و مطمئن شو قرمز میشود؛ اگر سبز شد، تست باگ را نمیگیرد و به کاری نمیآید. بعد اصلاح را اعمال کن و سبز شدنش را ببین.
نام تست باید دلیل وجودش را بگوید، نه فقط اسم تابع را. و بالای فایل یک کامنت کوتاه بگذار: باگ چه بود، کی دیده شد، و چه چیزی آن را نگرفت. نفر بعدی که این تست عجیب را میبیند وسوسه میشود حذفش کند؛ یک جمله مثل «این باگ ماهها زنده بود چون هیچ خطایی نمیداد» جلویش را میگیرد. این کامنتها به مرور حافظهی مهندسی پروژه میشوند؛ چیزی که در هیچ مستنداتی نوشته نمیشود.
پنج سناریو روی پرخطرترین مسیر
هدف پوشش بالا نیست؛ پنج تست درست روی جایی است که خطایش گران است. در کدبیس خودت مسیری را پیدا کن که خطا در آن پول یا داده میبرد، و برایش این پنج سناریو را بنویس:
- ورودی خالی، و بهویژه رشتهی خالی بهجای null.
- ورودی مرزی.
- اجرای دوباره با همان ورودی.
- وابستگی بیرونی که خراب است.
- عدد صفر یا منفی جایی که انتظار عدد مثبت میرود.
منطق قابل تست را از پایگاه داده جدا کن و هر سناریو را روی کد فعلی اجرا کن. دستکم یکی از این تستها باید روی کد فعلی قرمز شود؛ یعنی باگی پیدا کرده باشی که تا امروز کسی ندیده بود. اگر همه سبز شدند، سناریوهایت خوشبینانه بودهاند؛ سختترشان کن. سناریوی سوم معمولا بیشتر از بقیه باگ پیدا میکند، چون بیشتر کدها بیصدا فرض میکنند فقط یک بار صدا زده میشوند. سناریویی هم که نتوانستی تست کنی را بنویس و جلویش بگذار بهجای تست چه چیزی میگذاری: پایش، هشدار، یا بازبینی.
تست سبز یعنی بیخطر نیست
تست واحد مسیر پایگاه داده، شبکه و اجرای همزمان را پوشش نمیدهد. تست خوب خطر را کم میکند، صفر نمیکند، و کسی که خیال کند تست سبز یعنی بیخطر، از کسی که اصلا تست ندارد خطرناکتر است. اگر میخواهی تست نویسی را کنار پیدا کردن شکست خاموش، دیباگ و بازبینی خروجی دستیار کدنویسی روی کدبیس خودت تمرین کنی، جزئیات این دوره را ببین.