درک مفهوم API Mocking: سفری که ریکوئست HTTP طی میکند.

تشبیهی از سفر ریکوئست
تشبیهی از سفر ریکوئست

ءAPI Mocking یک تکنیک برای رهگیری ریکوئست‌های HTTP است که پاسخ به آن‌ها با جواب‌های Mock شده داده می‌شود.

ما اغلب از API Mocking در حین توسعه و تست برای افزایش کنترل شبکه، و مدل سازی آن متناسب با نیازهایمان استفاده می‌کنیم و مثل خیلی چیزهای دیگر، ما بر روی لایبرری‌های third-party برای اجرای قابل اعتماد ریکوئست تکیه می‌کنیم، و API Mocking ظرفیت‌هایی برایمان فراهم می‌کند.

اما تا حالا به این فکر کرده‌اید که این لایبرری‌ها چگونه کار می‌کنند؟

در این سری‌های کوچک، شما و من نگاهی عمیقی به اینکه چگونه API Mockingهای مختلف کار می‌کنند و همچنین خواهیم دید که هر کدام از آن‌ها چه نقاط ضعف و قوتی دارند، و بر روی گسترش درک‌مان از ریکوئست‌های HTTP در جاوااسکریپت کار می‌کنیم.

این مطلب ترجمه‌ای از Understanding API Mocking: The HTTP Request Journey است که امیدوارم بتوانم مفهوم را به خوبی برسانم و بقیه سری‌های آن را هم به محض انتشار ترجمه و منتشر کنم.



مراحل API Mocking

بطور خلاصه، API Mocking از دو مرحله متوالی تشکیل شده است:

  1. رهگیری یک ریکوئست HTTP ارسال شده (Intercepting an outgoing HTTP request;)
  2. پاسخ دادن به آن ریکوئست رهگیری شده با یک پاسخ mock شده.

راه های زیادی برای اجرای هر یک از این ها در یک API Mock مراحل وجود دارد.

برای ساختن API Mock واقعاً عالی، ما باید از تعادلی مناسب بین درستی کد و میزانی که این راه حل به ما اختیار کنترل می‌دهد مطمئن شویم؛ اما مهم‌تر از همه ما، باید بفهمیم که ریکوئست‌ها در جاوااسکریپت چگونه کار می‌کنند و همچنین تفاوت بین مراحل مختلفی که یک ریکوئست قبل از اجرا طی می‌کند، چگونه است.

و مانند خیلی موارد در زندگی، ما در این سفر در مورد همه این‌ها خواهیم آموخت.

سفر یک ریکوئست

تمام داستان‌ها شروع خودشان را دارند، و داستان ما هم با یک ریکوئست شروع می‌شود.

ببینید، رهگیری ریکوئست با نحوه انجام آن ریکوئست ارتباط تنگاتنگی دارد، اگر بخواهیم تفاوت و مبادله رویکردهای مختلف API Mocking را درک کنیم، ابتدا باید بفهمیم که چگونه ریکوئست‌های HTTP در جاوااسکریپت ساخته می‌شوند.

بنابراین، مطمئن شوید که headerها بسته شده و cookieها در ظرف پر شده، ما داریم به یک سفر ریکوئست می رویم.

کلاینت ریکوئست (The Request Client)

هر ریکوئستی با قصد خواندن یا تغییر دیتا شروع می‌شود.

ما این قصد را در کد قرار می‌دهیم و آن را برای یک request client برای اجرای آن فراهم می‌کنیم. اساساً هر APIای (native یا third-party) که به پذیرش یک درخواست و اجرای آن مربوط می‌شود، یک کلاینت ریکوئست است.

برای مثال، یکی از متداول‌ترین APIهای براوزر در اجرای ریکوئست‌ها Fetch Api است:

// We have an intention of fetching all movies.
// To describe that intention, we perform a &quotGET&quot request
// to the &quot/movies&quot endpoint on the server.
fetch('/movies')

در عمل، ممکن است از انواع کلاینت‌های مختلف استفاده کنید، مانند Axios یا React Query؛ و انتخاب شما معمولاً بستگی به نوع ریکوئستی که می‌خواهید توصیف کنید، دارد (مثلاً ممکن است شما بخواهید از یک کلاینت مخصوص GraphQL مانند Apollo برای توصیف ریکوئست‌های GraphQL استفاده کنید).

زمانی که کلاینت، ریکوئست ارسالی ما را بپذیرد، آن ابزاری برای نظارت بر اجرای آن ریکوئست به ما بر می‌گرداند، و در نهایت به ریسپانس دریافت شده از سرور رسیدگی می‌کند.

مثلاً برای بحث بالا، fetch یک Promise برای حل کردن یک ریسپانس بر می‌گرداند، نمونه‌ای که می‌توانیم بخوانیم:

// The fetch Promise resolves to a &quotResponse&quot instance
// that allows us to handle the response (e.g. get its
// status, headers, or read its body).
const response = await fetch('/movies')
console.log(response.ok, response.status)

برای ما توسعه‌دهندگان، کلاینت ریکوئست معمولاً جایی است که تعامل ما با ریکوئست به پایان می‌رسد؛ اما برای خود ریکوئست، این تازه شروع ماجراست.

در حالی که کلاینت‌های ریکوئست برای ما یک راه عالی برای اجرا و مدیریت آسان‌تر ریکوئست‌ها فراهم می‌کنند، آن‌ها فقط انتزاعی (abstractions) برای کد زیربنایی (underlying code) هستند که تمام کارهای سنگین را انجام می‌دهند.

و این دقیقاً همان جایی است که ریکوئست ما به آن جا می‌رود.

محیط (The Environment)

بدون توجه به کلاینت ریکوئست، ریکوئست ما ناگزیر به API استاندارد محیط مسئول رسیدگی به ریکوئست‌های HTTP می‌رسد.

هنگام استفاده از کلاینت ریکوئست third-party، آن تعامل را به کلاینت محول می‌کنیم، در حالی که ممکن است مستقیماً با آن API استاندارد تعامل داشته باشیم، و بنابراین ساخت و رسیدگی یک ریکوئست به جزئیات پیاده‌سازی کلاینت ریکوئست تبدیل می‌شود؛ برای مثال، وقتی از Axios استفاده می‌کنیم، ریکوئست ما را به عنوان XMLHttpRequest در براوزر و http.ClientRequest در Node.js نمایش می‌دهد، بدون آنکه ما حتی متوجه شویم.

اما ما امروز اینجا هستیم تا فراتر از انتزاع های third-party برویم و در مورد آن APIهای محیطی یاد بگیریم، اینطور نیست؟

مهم است در نظر داشته باشید که هر محیطی، ماژول شبکه خود را متفاوت پیاده‌سازی می‌کند. ما به عنوان مهندسان جاوااسکریپت بیشتر به محیط‌های براوزر و Node.js علاقه داریم.

بیایید نگاهی بیندازیم که چه native APIهایی برای ارائه ریکوئست وجود دارد.

مرورگر (Browser)

window.fetch

ءFetch Api یکی از رایج‌ترین راه‌ها برای درخواست در وب است.

در سال ۲۰۱۵ به جهان معرفی شد و به عنوان گامی رو به جلو از XMLHttpRequestراه اندازی شد، و بدون شک زندگی ما توسعه‌دهندگان را برای بهتر شدن تغییر داد.

fetch('https://api.example.com/users', {
   method: 'POST',
   headers: { 'Content-Type': 'application/json' },
   body: JSON.stringify({ name: 'Alice' })
})

پیاده‌سازی براوزر برای Fetch در کد native C است و فراخوانی window.fetch() آخرین سطحی‌ است که می‌توانیم با آن تعامل داشته باشیم.

window.XMLHttpRequest

ءXMLHttpRequest اولین حضور عمومی خود به عنوان یک API Fully-functional برای ارسال ریکوئست‌ها را در سال ۲۰۰۲ داشت.

این ممکن است برای برخی غافل گیرکننده باشد، اما XHR هنوز هم در براوزرها تا به امروز ارسال می‌شود و یک راه بر حق برای ساختن ریکوئست‌هاست! بیش از این، XHR چند قابلیت را به نمایش می‌گذارد که حتی Fetch مدرن هم آن‌ها را ندارد، مانند مانیتور کردن فرآیند یک ریکوئست و کنسل شدن ریکوئست، که قبل از تبدیل شدن AbortControllerبه یک چیز، از طریق Fetch امکان پذیر نبود.

const request = new XMLHttpRequest()
request.open('POST', 'https://api.example.com/users')
request.setRequestHeader('Content-Type', 'application/json')
request.write(JSON.stringify({ name: 'Alice' })
request.send()

شبیه بهwindow.fetch،ریشه‌های XMLHttpRequest نیز به عمق کد native browser بدون هیچ لایه میانی می‌روند تا منطق mocking ما را اتصال دهند.

Node.js

http.request (https.request)

ء Node.js برای ما یک API سطح بالا برای انجام ریکوئست‌ها از طریق ماژول‌های httpو httpsفراهم کرده است؛ در حالی که آن‌ها متدهای مشابهی را به اشتراک می‌گذارند، مانند .get()و .request()، اما پیاده‌سازی آن‌ها متفاوت است؛ زیرا ریکوئست پروتکل‌های متفاوتی را مدیریت می‌کنند.

import https from 'https'
const request = https.request('https://api.example.com/users', {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' }
})
request.write(JSON.stringify({ name: 'Alice' }))
request.end()

ء API http.request()چیزی است که اکثر کلاینت ریکوئست‌ها در Node.js به صورت داخلی از آن استفاده می‌کنند.

شاید گفتن اینکه APIای که باعث تولد خیلی از کلاینت ریکوئست‌ها در آن محیط شده همین API است، خیلی پر حرفی باشد (فرض وحشیانه من در اینجا).

http.ClientRequest

حرکت بیشتر به سمت عمق call stack، ءhttp.request ریکوئست‌ها را با استفاده از کلاس http.ClientRequestنشان می دهد (و رسیدگی می کند).

منصفانه است ذکر شود که برخی از لایبرری‌ها و polyfillها مستقیماً از این کلاس استفاده می‌کنند و به طور کلی API سطح بالاتر را دور می‌زنند.

همانطور که گفته شد، ساخت یک ریکوئست از طریق http.ClientRequestشبیه به استفاده از http.requestباشد، و حتی ممکن است گاهی اوقات این تفاوت، با چشم غیر مسلح قابل مشاهده نباشد:

import http from 'http'
import https from 'https'
const req = new http.ClientRequest({
     protocol: 'https:',
     host: 'api.example.com',
     pathname: '/users',
     headers: {
        'Content-Type': 'application/json',
     },
     agent: new https.Agent(),
})
req.write(JSON.stringify({ name: 'Alice' }))
req.end()
نکته آنکه این همان کلاس http.ClientRequestاست که هر دو ریکوئست HTTP و HTTPS را توصیف می‌کند. برخلاف تمایز http.requestو https.requestدر سطح بالاتر.


net.Socket

حتی عمیق‌تر برویم. هر ریکوئست HTTP لزوماً داده‌های منتقل شده از طریق یک سوکت هستند و Node.js هم یک کلاس net.Socket برای توصیف آن دارد.

import { Socket } from 'net'
const socket = new Socket()
socket.connect(443, 'api.example.com', () => {
     socket.write('POST /users HTTP/1.0\n')
     socket.write('Content-Type: application/json\n')
     socket.end(JSON.stringify({ name: 'Alice' }))
})

این واقعیت که ما پیام‌های HTTP خام را از طریق سیم ارسال می‌کنیم، باید تصور خوبی از سطح پایین Socket API به شما بدهد.

هر چند بعید است که مستقیماً از این API استفاده کنید، اما همچنان بخشی اجتناب ناپذیر از سفر ریکوئست در Node.js است و در نتیجه برای انتخاب ما از رویکرد API Mocking مهم است.

تذکرات مفید

در Node.js نسخه 17 fetchAPI مشابه با آنچه که در براوزر وجود دارد، اضافه شده.

از آن جایی که آن‌ها با مشخصات یکسانی مطابقت دارند، fetchدر Node.js به ترتیب ریکوئست‌ها و ریسپانس‌ها را با استفاده از کلاس‌های Requestو Responseنشان می‌دهد، اما آن‌ها همچنان به‌ عنوان انتزاع بر روی APIهای سطح پایینی که در بالا به آن اشاره کردیم، عمل می‌کنند.

هنگامی که ماژول شبکه کار خود را انجام داد، در نهایت ریکوئست انجام می‌شود و محیط را ترک می کند و از فیبر به سمت سرور درخواست شده حرکت می‌کند.

سرور

سرور جایی است که ریکوئست‌ها برای حل و فصل نهایی ارسال می‌شوند.

سرور دورترین منطقه از ارسال ریکوئست ما است؛ زیرا محیطی متفاوت است که اغلب به زبانی متفاوت از برنامه ما پیاده‌سازی می‌شود و پیچیدگی‌های خودش را دارد. هر چند هنگامی که سرور ریکوئست را حل کرد، ریسپانس را از طریق همان سفر ارسال می‌کند تا در نهایت به کلاینت ریکوئست بازگردد.

بنابراین، هنگامی که ریکوئست ما به سرور می‌رسد، ما نمی‌توانیم کاری انجام دهیم تا از زمان اجرای سمت کلاینت بر آن ریکوئست تأثیر بگذاریم.



جمع بندی

هدف هر سفر بهبود و تغییر کسانی است که به اندازه کافی شجاع هستند که وارد آن شوند.

و سفر ریکوئستی که ما به تازگی پشت سر گذاشتیم نیز از این قاعده مستثنی نیست. همانطور که در مورد روش‌های مختلف توصیف ریکوئست‌ها و نقاط بازرسی هر ریکوئست آموختیم، می‌توانیم پیش‌نویس راه‌های ممکن برای اجرای API Mocking در برنامه‌مان را ادامه دهیم.

این دقیقاً همان چیزی است که در پست بعدی به آن خواهم پرداخت.



ممنون از وقتی که گذاشتین و این مطلب رو مطالعه کردین.

منتظر نظرات و پیشنهادات شما هستم تا بتونم بر روی مطالب بعدی بهتر کار کنم.