تست آسان HTTP Client در انجمن Go – DEV

معرفی
به عنوان یک مهندس نرم افزار، احتمالاً با نوشتن کد برای تعامل با سرویس های HTTP خارجی آشنا هستید. به هر حال، این یکی از رایج ترین کارهایی است که ما انجام می دهیم! برنامههای ما تقریباً همیشه شامل درخواستهای HTTP خارجی میشوند، چه واکشی دادهها، پردازش پرداختها با ارائهدهنده، یا خودکار کردن پستهای رسانههای اجتماعی. برای اینکه نرم افزار ما قابل اعتماد و قابل نگهداری باشد، به روشی برای آزمایش کد مسئول اجرای این درخواست ها و رسیدگی به خطاهایی که ممکن است رخ دهد نیاز داریم. این ما را با چند گزینه باقی می گذارد:
- یک بسته بندی کلاینت را پیاده سازی کنید که می تواند توسط کد برنامه اصلی مورد تمسخر قرار گیرد، که همچنان در آزمایش شکافی ایجاد می کند.
- تجزیه و مدیریت پاسخ تست جدا از اجرای درخواست واقعی. در حالی که احتمالاً ایده خوبی است که این واحد سطح پایین را به صورت جداگانه آزمایش کنید، خوب است اگر به راحتی بتوان آن را همراه با درخواست های واقعی پوشش داد.
- انتقال آزمایشها به تست یکپارچهسازی که میتواند توسعه را کند کند و قادر به آزمایش برخی سناریوهای خطا نیست و ممکن است تحت تأثیر قابلیت اطمینان سایر خدمات قرار گیرد.
این گزینه ها وحشتناک نیستند، به خصوص اگر بتوان همه آنها را با هم استفاده کرد، اما ما یک گزینه بهتر داریم: تست VCR.
تست VCR که به نام ضبط کننده کاست ویدیویی نامگذاری شده است، نوعی آزمایش ساختگی است که تجهیزات آزمایشی را از درخواست های واقعی تولید می کند. فیکسچرها درخواست و پاسخ را برای استفاده مجدد به صورت خودکار در تست های آینده ثبت می کنند. اگرچه ممکن است بعد از آن مجبور شوید وسایل را تغییر دهید تا ورودیهای مبتنی بر زمان پویا را مدیریت کنید یا اعتبارنامهها را حذف کنید، این کار بسیار سادهتر از ساختن ساختن از ابتدا است. چند مزیت اضافی برای تست VCR وجود دارد:
- کد خود را تا سطح HTTP اجرا کنید تا بتوانید برنامه خود را از انتها به انتها آزمایش کنید
- برای آزمایش سناریوهای خطا که اغلب به صورت ارگانیک رخ نمیدهند، میتوانید پاسخهای دنیای واقعی بگیرید و وسایل تولید شده را برای افزایش زمان پاسخ، ایجاد محدودیت نرخ و غیره تغییر دهید.
- اگر کد شما از یک بسته/کتابخانه خارجی برای تعامل با یک API استفاده میکند، ممکن است دقیقاً ندانید که یک درخواست و پاسخ چگونه است، بنابراین آزمایش VCR میتواند به طور خودکار آن را بفهمد.
- فیکسچرهای تولید شده همچنین می توانند برای تست های اشکال زدایی استفاده شوند و مطمئن شوید کد شما درخواست مورد انتظار را اجرا می کند.
شیرجه عمیق تر با استفاده از Go
اکنون که انگیزه پشت تست VCR را می بینید، بیایید عمیق تر به نحوه پیاده سازی آن در Go با استفاده از dnaeon/go-vcr
.
این کتابخانه به طور یکپارچه با هر کد مشتری HTTP ادغام می شود. اگر کد کتابخانه مشتری شما قبلاً اجازه تنظیم را نمی دهد *http.Client
یا مشتری http.Transport
، اکنون باید آن را اضافه کنید.
برای کسانی که آشنا نیستند، یک http.Transport
اجرای است http.RoundTripper
، که در اصل یک میان افزار سمت کلاینت است که می تواند به درخواست/پاسخ دسترسی پیدا کند. برای اجرای مجدد خودکار روی پاسخهای 500 سطحی یا 429 (محدودیت نرخ) یا افزودن معیارها و ثبت اطلاعات در مورد درخواستها مفید است. در این صورت اجازه می دهد go-vcr
برای ارسال مجدد درخواست ها به سرور HTTP در حال پردازش خودش.
مثال کوتاه کننده URL
بیایید با یک مثال ساده شروع کنیم. ما میخواهیم بستهای ایجاد کنیم که درخواستهایی را برای https://cleanuri.com API رایگان ارسال کند. این بسته یک عملکرد را ارائه می دهد: Shorten(string) (string, error)
از آنجایی که این یک API رایگان است، شاید بتوانیم آن را با درخواست مستقیم به سرور آزمایش کنیم؟ این ممکن است کار کند، اما می تواند منجر به چند مشکل شود:
- سرور دارای محدودیت نرخ 2 درخواست در ثانیه است که اگر آزمایش های زیادی داشته باشیم می تواند مشکل ساز باشد.
- اگر سرور از کار بیفتد یا مدتی طول بکشد تا پاسخ دهد، آزمایشات ما ممکن است شکست بخورد
- اگرچه URL های کوتاه شده کش هستند، اما هیچ تضمینی نداریم که هر بار خروجی یکسانی را دریافت کنیم
- ارسال ترافیک غیر ضروری به یک API رایگان فقط بی ادبی است!
خوب، اگر یک رابط ایجاد کنیم و آن را مسخره کنیم، چه؟ بسته ما فوق العاده ساده است، بنابراین این امر بیش از حد آن را پیچیده می کند. از آنجایی که پایین ترین سطح مورد استفاده ما این است *http.Client
، ما باید یک رابط جدید در اطراف آن تعریف کنیم و یک mock پیاده سازی کنیم.
گزینه دیگر این است که URL هدف را برای استفاده از یک پورت محلی که توسط آن ارائه می شود، لغو کنید httptest.Server
. این اساسا یک نسخه ساده شده از چه چیزی است go-vcr
انجام می دهد و در مورد ساده ما کافی است، اما در سناریوهای پیچیده تر قابل نگهداری نخواهد بود. حتی در این مثال، خواهید دید که چگونه مدیریت فیکسچرهای تولید شده ساده تر از مدیریت پیاده سازی های مختلف سرور ساختگی است.
از آنجایی که رابط کاربری ما از قبل تعریف شده است و ما مقداری ورودی/خروجی معتبر را از امتحان رابط کاربری در https://cleanuri.com می دانیم، این یک فرصت عالی برای تمرین توسعه آزمایش محور است. ما با اجرای یک تست ساده برای خود شروع خواهیم کرد Shorten
تابع:
package shortener_test
func TestShorten(t *testing.T) {
shortened, err := shortener.Shorten("https://dev.to/calvinmclean")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if shortened != "https://cleanuri.com/7nPmQk" {
t.Errorf("unexpected result: %v", shortened)
}
}
خیلی راحت! ما می دانیم که آزمون در کامپایل شکست خواهد خورد زیرا shortener.Shorten
تعریف نشده است، اما به هر حال آن را اجرا می کنیم، بنابراین رفع آن رضایت بخش تر خواهد بود.
در نهایت، بیایید جلو برویم و این تابع را پیاده سازی کنیم:
package shortener
var DefaultClient = http.DefaultClient
const address = "https://cleanuri.com/api/v1/shorten"
// Shorten will returned the shortened URL
func Shorten(targetURL string) (string, error) {
resp, err := DefaultClient.PostForm(
address,
url.Values{"url": []string{targetURL}},
)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode)
}
var respData struct {
ResultURL string `json:"result_url"`
}
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
return "", err
}
return respData.ResultURL, nil
}
حالا آزمون ما قبول شد! به همان اندازه که قول داده بودم راضی کننده است.
برای شروع استفاده از VCR، باید Recorder را مقداردهی اولیه کرده و override کنیم shortener.DefaultClient
در ابتدای آزمون:
func TestShorten(t *testing.T) {
r, err := recorder.New("fixtures/dev.to")
if err != nil {
t.Fatal(err)
}
defer func() {
require.NoError(t, r.Stop())
}()
if r.Mode() != recorder.ModeRecordOnce {
t.Fatal("Recorder should be in ModeRecordOnce")
}
shortener.DefaultClient = r.GetDefaultClient()
// ...
تست را برای تولید اجرا کنید fixtures/dev.to.yaml
با جزئیات در مورد درخواست و پاسخ آزمون. هنگامی که تست را دوباره اجرا می کنیم، به جای تماس با سرور، از پاسخ ضبط شده استفاده می کند. فقط حرف من را قبول نکنید. وای فای کامپیوتر خود را خاموش کنید و دوباره تست ها را اجرا کنید!
همچنین ممکن است متوجه شوید که زمان لازم برای اجرای آزمون نسبتاً ثابت است go-vcr
مدت زمان پاسخ را ضبط و پخش می کند. شما می توانید به صورت دستی این فیلد را در YAML تغییر دهید تا سرعت تست ها افزایش یابد.
خطاهای تمسخر
برای نشان دادن بیشتر مزایای این نوع آزمایش، اجازه دهید ویژگی دیگری را اضافه کنیم: بعد از آن دوباره امتحان کنید 429
پاسخ به دلیل محدودیت نرخ از آنجایی که می دانیم محدودیت نرخ API بر ثانیه است، Shorten
می تواند به طور خودکار یک ثانیه صبر کند و در صورت دریافت a، دوباره امتحان کند 429
کد پاسخ.
من سعی کردم این خطا را مستقیماً با استفاده از API بازتولید کنم، اما به نظر می رسد قبل از در نظر گرفتن محدودیت نرخ، با URL های موجود از حافظه پنهان پاسخ می دهد. به جای اینکه حافظه پنهان را با URL های جعلی آلوده کنیم، این بار می توانیم مسخره های خود را ایجاد کنیم.
این یک فرآیند ساده است زیرا ما قبلاً وسایلی را تولید کرده ایم. پس از کپی/پیست کردن fixtures/dev.to.yaml
در یک فایل جدید، تعامل موفقیت آمیز درخواست/پاسخ را کپی کنید و کد پاسخ اول را از آن تغییر دهید 200
به 429
. این ثابت یک تلاش مجدد موفق پس از شکست محدود کننده نرخ را تقلید می کند.
تنها تفاوت این تست با تست اصلی، نام فایل فیکسچر جدید است. خروجی مورد انتظار از آن زمان یکسان است Shorten
باید خطا را مدیریت کند این بدان معنی است که ما می توانیم آزمایش را در یک حلقه قرار دهیم تا آن را پویاتر کنیم:
func TestShorten(t *testing.T) {
fixtures := []string{
"fixtures/dev.to",
"fixtures/rate_limit",
}
for _, fixture := range fixtures {
t.Run(fixture, func(t *testing.T) {
r, err := recorder.New(fixture)
if err != nil {
t.Fatal(err)
}
defer func() {
require.NoError(t, r.Stop())
}()
if r.Mode() != recorder.ModeRecordOnce {
t.Fatal("Recorder should be in ModeRecordOnce")
}
shortener.DefaultClient = r.GetDefaultClient()
shortened, err := shortener.Shorten("https://dev.to/calvinmclean")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if shortened != "https://cleanuri.com/7nPmQk" {
t.Errorf("unexpected result: %v", shortened)
}
})
}
}
یک بار دیگر، آزمون جدید شکست خورده است. این بار به دلیل عدم رسیدگی 429
پاسخ، بنابراین بیایید ویژگی جدید را برای قبولی در آزمون پیاده سازی کنیم. به منظور حفظ سادگی، تابع ما خطا را با استفاده از آن کنترل می کند time.Sleep
و یک تماس بازگشتی به جای پرداختن به پیچیدگی در نظر گرفتن حداکثر تلاش مجدد و عقب نشینی نمایی:
func Shorten(targetURL string) (string, error) {
// ...
switch resp.StatusCode {
case http.StatusOK:
case http.StatusTooManyRequests:
time.Sleep(time.Second)
return Shorten(targetURL)
default:
return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode)
}
// ...
حالا دوباره تست ها را اجرا کنید و ببینید که موفق می شوند!
خودتان یک قدم جلوتر بروید و آزمایشی برای یک درخواست بد اضافه کنید، که هنگام استفاده از یک URL نامعتبر مانند my-fake-url
.
کد کامل این مثال (و تست درخواست بد) در Github موجود است.
نتیجه
مزایای تست VCR فقط از همین مثال ساده مشخص است، اما زمانی که با برنامههای پیچیدهای که درخواستها و پاسخها سختگیرانه هستند، کار میکنند، حتی تاثیر بیشتری دارند. بهجای پرداختن به مسخرههای خستهکننده یا انتخاب هیچ آزمونی، توصیه میکنم این را در برنامههای خود امتحان کنید. اگر از قبل به تستهای یکپارچهسازی متکی هستید، شروع با VCR حتی سادهتر است، زیرا از قبل درخواستهای واقعی دارید که میتوانند فیکسچر ایجاد کنند.
اسناد و نمونه های بیشتر را در مخزن Github بسته بررسی کنید: https://github.com/dnaeon/go-vcr