توسعه رفتارمحور (BDD)
در BDD برنامهنویس، تستر و کارشناس کسبوکار قبل از نوشتن کد با هم چند مثال واقعی میسازند. این مثالها به شکل Given، When، Then نوشته میشوند و به تست خودکار تبدیل میشوند. نتیجه، مستنداتی است که همیشه با کد هماهنگ است.
نویسنده: bezzad
مشکل: کد درست، ولی چیز اشتباه
کارشناس فروش میگوید: «سفارشهای بالای ۵۰۰ هزار تومان ارسال رایگان دارند.»
برنامهنویس کد را مینویسد و تست هم مینویسد. همه تستها سبزند. ولی بعد از انتشار، کارشناس فروش ناراضی است:
- منظور از «بالای ۵۰۰ هزار» چه بود؟ برنامهنویس فکر کرد ۵۰۰ هزار خودش رایگان نیست. کارشناس فکر میکرد هست.
- مبلغ قبل از تخفیف یا بعد از آن؟ برنامهنویس مبلغ قبل از تخفیف را حساب کرد. کارشناس مبلغ بعد از تخفیف را میخواست.
- تستها چرا خطا را نگرفتند؟ چون تستها برداشت برنامهنویس را چک میکردند، نه خواسته کسبوکار را.
مشکل در کد نبود. مشکل در فهم مشترک بود.
ایده BDD: گفتگو با مثالهای واقعی
توسعه رفتارمحور (Behavior-Driven Development) از همین مشکل شروع میشود. قبل از نوشتن کد، سه نقش با هم یک جلسه کوتاه دارند: کسی که کسبوکار را میشناسد، کسی که کد مینویسد و کسی که تست میکند. به این جلسه گاهی «سه دوست» (Three Amigos) میگویند.
در این جلسه به جای قانون کلی، مثالهای دقیق میسازیم:
- سبد ۴۹۹ هزار تومانی، بعد از تخفیف: هزینه ارسال ۵۰ هزار تومان.
- سبد ۵۰۰ هزار تومانی، بعد از تخفیف: ارسال رایگان.
- سبد ۶۰۰ هزار تومانی با ۲۰ درصد تخفیف، یعنی ۴۸۰ هزار: هزینه ارسال ۵۰ هزار تومان.
همین سه مثال هر دو سوءتفاهم بالا را قبل از نوشتن حتی یک خط کد پیدا میکنند.
زبان Given، When، Then
مثالها را با یک شکل ثابت مینویسیم. به این شکل Gherkin میگوییم:
- در این حالت (Given). وضعیت اول سیستم. مثلاً جمع سبد.
- وقتی (When). کاری که کاربر انجام میدهد. مثلاً پرداخت سبد.
- آنگاه (Then). نتیجهای که انتظار داریم. مثلاً هزینه ارسال.
این همان سه بخش تست واحد است: آماده کردن، اجرا و بررسی. فرقش این است که به زبان کسبوکار نوشته شده و کارشناس فروش هم میتواند آن را بخواند.
Feature: Free shipping
Orders of 500000 toman or more ship for free.
The rule uses the total after discount.
Scenario: Order just below the limit pays shipping
Given the cart total is 499000 toman
When the customer checks out
Then the shipping cost is 50000 toman
Scenario Outline: Shipping cost by cart total
Given the cart total is <total> toman
When the customer checks out
Then the shipping cost is <shipping> toman
Examples:
| total | shipping |
| 200000 | 50000 |
| 500000 | 0 |
| 480000 | 50000 |
با Scenario Outline یک سناریو را با چند ردیف داده اجرا میکنیم. هر ردیف جدول یک مثال است.
وصل کردن سناریو به کد
فایل feature به تنهایی فقط متن است. یک ابزار باید هر خط آن را به یک متد C# وصل کند. در .NET، ابزار رایج امروز Reqnroll است. ابزار قدیمیتر SpecFlow در پایان سال ۲۰۲۴ کنار گذاشته شد. ابزار Reqnroll از کد همان پروژه ساخته شده است و مهاجرت به آن ساده است.
[Binding]
public sealed class ShippingSteps
{
private decimal _cartTotal;
private decimal _shipping;
[Given("the cart total is {int} toman")]
public void GivenCartTotal(int total) => _cartTotal = total;
[When("the customer checks out")]
public void WhenCustomerChecksOut() =>
_shipping = ShippingCost.For(_cartTotal, isVip: false);
[Then("the shipping cost is {int} toman")]
public void ThenShippingCostIs(int expected) => Assert.Equal(expected, _shipping);
}
کلمه int داخل آکولاد، عدد داخل جمله را پیدا میکند و به پارامتر متد میدهد. پس یک متد برای همه سناریوهایی کافی است که همین جمله را با عدد دیگر دارند.
سناریوی خوب، سناریوی بد
سناریو باید رفتار را بگوید، نه قدمهای کلیک کردن را.
بددستوری و وابسته به صفحه
Given I open "/cart"
And I click "#add-item-7"
And I type "2" into "#qty"
When I click "#checkout"
Then I see "50,000" in ".shipping"اگر طراحی صفحه عوض شود، سناریو میشکند. کارشناس فروش هم از آن چیزی نمیفهمد.
خوبتوصیفی و به زبان کسبوکار
Given the cart total is 499000 toman
When the customer checks out
Then the shipping cost is 50000 tomanفقط قانون را میگوید. چطور اجرا شدنش، کار متدهای کد تست است.
مقایسه BDD و TDD
| TDD | BDD | |
|---|---|---|
| سؤال اصلی | آیا کد درست کار میکند؟ | آیا چیز درستی میسازیم؟ |
| چه کسی مینویسد؟ | برنامهنویس | سه نقش با هم |
| زبان تست | کد C# | جملههای Given، When، Then |
| اندازه | معمولاً یک کلاس یا یک قانون کوچک | معمولاً یک قابلیت کامل |
این دو رقیب هم نیستند. میشود سناریوی BDD را برای یک قابلیت نوشت و داخل آن، هر کلاس را با TDD ساخت.
قانونهای مهم
- اول گفتگو، بعد فایل. اگر برنامهنویس به تنهایی فایل feature بنویسد، فقط یک تست با ظاهر متفاوت ساخته است.
- هر سناریو یک رفتار. سناریوی طولانی با ده قدم When را به چند سناریو تقسیم کن.
- زبان کسبوکار، نه زبان فنی. در سناریو اسم جدول، آدرس صفحه و کد HTML نباشد.
- همه سناریوها از راه صفحه اجرا نشوند. متدهای کد تست میتوانند مستقیم کد دامنه یا API را صدا بزنند. این خیلی سریعتر و پایدارتر است.
- جملهها را یکسان نگه دار. اگر یک جمله را ده شکل مختلف بنویسیم، ده متد تکراری لازم میشود.
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| نوشتن feature بدون حضور کسبوکار | سوءتفاهمها همچنان پیدا نمیشوند. | جلسه کوتاه با مثال قبل از کد. |
| سناریوهای کلیکی و وابسته به صفحه | با هر تغییر ظاهر، تستها میشکنند. | سناریوی توصیفی، اجرا از راه API یا دامنه. |
| استفاده از Reqnroll برای همه تستها | لایه اضافه بدون سود. هیچ کس جز برنامهنویس feature ها را نمیخواند. | فقط برای قانونهایی که کسبوکار باید ببیند. |
| جملههای کلی مثل «سیستم درست کار میکند» | سناریو هیچ چیزی را دقیق چک نمیکند. | عدد و نتیجه دقیق در هر مثال. |
چه وقت BDD؟
مناسب
- قانونهای کسبوکار پیچیده که سوءتفاهم در آنها گران تمام میشود.
- تیمی که کارشناس کسبوکار در دسترس دارد و او واقعاً سناریوها را میخواند.
- حوزههایی مثل بیمه، مالیات، قیمتگذاری و پرداخت.
نامناسب
- کد فنی بدون قانون کسبوکار، مثل یک کتابخانه کش.
- تیمی که فقط ابزار را نصب میکند، ولی هیچ گفتگویی ندارد.
- قانونهای ساده که یک تست واحد خوب برایشان کافی است.
خلاصه در شش خط
- خیلی از باگها از سوءتفاهم میآیند، نه از کد اشتباه.
- در BDD سه نقش قبل از کد با هم مثالهای دقیق میسازند.
- مثالها با Given، When، Then نوشته میشوند تا همه بتوانند آنها را بخوانند.
- در .NET، ابزار Reqnroll هر جمله را به یک متد C# وصل میکند.
- سناریوی خوب رفتار را میگوید، نه کلیکها را.
- بخش اصلی BDD گفتگو است. بدون گفتگو، ابزار فقط هزینه اضافه است.