تزریق وابستگی پیشرفته در اکسیر با Rewire

در آخرین پست خود، بررسی کردیم که چگونه Dependency Injection (DI) یک الگوی طراحی قدرتمند است که می تواند تست های ExUnit ما را بهبود بخشد.
در این مقاله، با تمرکز بر کتابخانه Rewire برای پروژههای Elixir، عمیقتر به موضوع DI در اکسیر خواهیم پرداخت.
ما مفاهیم اصلی Rewire، نحوه شروع کار با آن و مثال های عملی را پوشش خواهیم داد. همچنین نحوه استفاده از Rewire را در کنار Mox خواهیم دید.
بیا شروع کنیم!
مقدمه ای بر Rewire
یکی از چالشهایی که در مقاله قبلی با آن مواجه بودیم، نبود روشی ساختاریافته برای تعریف و تزریق وابستگیها به ماژولهایمان بود. ما مجبور شدیم به صورت دستی مدل های خود را برای آزمایش تعریف کنیم.
اینجاست که Rewire و Mox وارد بازی می شوند:
- سیم کشی مجدد روشی ساختارمندتر و انعطاف پذیرتر برای پیاده سازی DI در پروژه های اکسیر ارائه می دهد.
- موکس کتابخانه ای است که به ما امکان می دهد برای تست های خود ماک تعریف کنیم.
ترکیب این دو ابزار می تواند به طور قابل توجهی تست پذیری و مدولار بودن برنامه های Elixir ما را بهبود بخشد.
بیایید با راه اندازی یک پروژه نمونه که از هر دو کتابخانه استفاده می کند، شروع کنیم.
چرا از Rewire و Mox برای Elixir استفاده کنیم؟
برای جمعبندی قسمت اول این مجموعه، مزایای DI برای آزمایشپذیری و مدولار بودن را مورد بحث قرار دادیم. دیدیم که چگونه میتوانیم از وابستگیهای عبور از طریق پارامترهای تابع استفاده کنیم:
- ما … را داریم
EmailScanner
ماژولی که بر aSpamFilterService
برای بررسی اینکه آیا ایمیل اسپم است یا خیر:
defmodule EmailScanner do
def scan_email(spam_filter_service, email) do
spam_filter_service.check_spam(email)
end
end
- ما … را داریم
SpamFilterService
ماژولی که منطق بررسی هرزنامه را پیاده سازی می کند:
defmodule SpamFilterService do
def check_spam(email_content) do
String.contains?(email_content, "spam")
end
end
- ما همچنین یک
MockSpamFilterService
ماژولی که اجرا می کندSpamFilterService
رفتار برای اهداف آزمایشی:
defmodule MockSpamFilterService do
def check_spam(_email), do: false
end
- در نهایت، ما یک تست داریم که از آن استفاده می کند
MockSpamFilterService
برای تست کردنEmailScanner
مدول:
defmodule EmailScannerTest do
use ExUnit.Case
test "scan_email with non-spam email returns false" do
non_spam_email = %Email{content: "Hello, world!"}
assert false == EmailScanner.scan_email(MockSpamFilterService, non_spam_email)
end
end
در Elixir، ماژول ها بدون حالت هستند، بنابراین راه اصلی برای انتقال وابستگی ها به یک ماژول از طریق پارامترهای تابع است. در حالی که ماژولهای Elixir میتوانند ویژگیهایی داشته باشند، اما برای اطلاعات و ابردادههای زمان کامپایل استفاده میشوند، نه برای نگهداری حالت زمان اجرا.
را بگیرید EmailScanner
به عنوان مثال ماژول باید بگذریم SpamFilterService
به عنوان پارامتری برای scan_email
تابع. این غیرضروری است، زیرا تنها دلیل داشتن این پارامتر تابع، آزمایش پذیر ساختن ماژول است.
علاوه بر این، چند مشکل با خوانایی کد و ناوبری ایجاد می کند:
- زیرا ماژول در انتظار است
SpamFilterService
به عنوان یک پارامتر، ما نمی توانیم به راحتی ببینیم که ماژول به چه چیزی بستگی دارد. - کامپایلر نمیتواند مشکلات مربوط به اجرای ماژول را پیدا کند، زیرا ما میتوانیم هر ماژولی را که آن را پیادهسازی میکند، پاس کنیم
SpamFilterService
رفتار – اخلاق.
این رویکرد ممکن است برای پروژه های کوچک به خوبی کار کند، اما با رشد پروژه ما، ممکن است متوجه شویم که همان الگو را بارها و بارها تکرار می کنیم. با Rewire، ما نباید نگران این مسائل باشیم. ما فقط میتوانیم روی نوشتن کدهای تمیز و قابل نگهداری تمرکز کنیم و در عین حال نگرانیهای مربوط به آزمایش، تمسخرها و موارد خرد را در فایلهای آزمایشی خود حفظ کنیم.
شروع کار با Rewire و Mox در پروژه اکسیر شما
بیایید اکنون به استفاده از Rewire و Mox در عمل بپردازیم.
مرحله 1: یک پروژه اکسیر جدید ایجاد کنید
قبل از ترکیب Rewire و Mox، یک پروژه Elixir جدید ایجاد کنید:
mix new email_scanner
این دستور یک پروژه Elixir جدید به نام تولید می کند email_scanner
، از جمله ساختار درخت نظارت.
مرحله 2: افزودن وابستگی ها
برای استفاده از Rewire و Mox، باید آنها را به وابستگی های پروژه خود اضافه کنید. خود را به روز کنید mix.exs
به صورت زیر فایل کنید:
defp deps do
[
{:rewire, "~> 1.0", only: :test},
{:mox, "~> 1.0", only: :test}
]
end
پس از به روز رسانی وابستگی ها، اجرا کنید mix deps.get
در ترمینال خود برای واکشی و نصب آنها.
بعد، اجازه دهید دو ماژول اصلی خود را تعریف کنیم:
defmodule EmailScanner do
def filter_email(email) do
email
|> mark_as_important()
|> SpamFilterService.check_spam()
end
defp mark_as_important(email) do
important_senders = ["boss@example.com", "hr@example.com"]
updated_email =
if Enum.any?(important_senders, fn sender -> sender == email.sender end) do
%{email | important: true}
else
email
end
updated_email
end
end
ما مثال کد خود را کمی واقعی تر می کنیم. این filter_email
تابع ایمیلهای ارسالکنندههای مهم را بهعنوان مهم علامتگذاری میکند و بررسی میکند که ایمیلها با استفاده از هرزنامه هستند یا خیر SpamFilterService
مدول.
در مرحله بعد، ما را تعریف می کنیم SpamFilterService
مدول:
defmodule SpamFilterService do
def check_spam(email_content) do
String.contains?(email_content, "spam")
end
end
بیایید یک تست اساسی برای آن ایجاد کنیم EmailScanner
مدول:
defmodule EmailScannerTest do
use ExUnit.Case
describe "filter_email/2" do
test "marks email as important from specific sender and checks for spam" do
important_sender_email = %{sender: "boss@example.com", content: "Please review the attached report.", important: false}
non_important_sender_email = %{sender: "random@example.com", content: "Check out these deals!", important: false}
# Filtering emails sent from the important sender
assert %{important: true, is_spam: false} = EmailScanner.filter_email(important_sender_email)
# Filtering emails sent from a non-important sender
assert %{important: false, is_spam: false} = EmailScanner.filter_email(non_important_sender_email)
end
end
end
در کد بالا، EmailScanner
ماژول متکی بر SpamFilterService
ماژول برای بررسی اینکه آیا یک ایمیل اسپم است یا خیر. با این حال، ما نمی توانیم آن را آزمایش کنیم EmailScanner
ماژول بدون آزمایش SpamFilterService
ماژول، که ایده آل نیست.
باید مسخره کنیم SpamFilterService
ماژول تا بتوانیم آن را تست کنیم EmailScanner
ماژول به صورت مجزا
مرحله 3: پیکربندی Mox
Mox به مقداری تنظیمات در پیکربندی آزمایشی شما نیاز دارد. a را باز کنید یا ایجاد کنید test/test_helper.exs
فایل کنید و خط زیر را اضافه کنید تا یک مدل ساختگی بر اساس پروتکل یا رفتاری که پروژه شما استفاده می کند تعریف کنید:
ExUnit.start()
Mox.defmock(EmailScanner.SpamFilterServiceMock, for: EmailScanner.SpamFilterService)
Mox ساخت مسخرههای مبتنی بر رفتارها یا پروتکلها را برای ما آسان میکند، که برای آزمایش ماژولهایی که بر این انتزاعها متکی هستند ضروری است.
زمانی که ماک ما تعریف شد، میتوانیم به جای پیادهسازی واقعی از آن در تستهای خود استفاده کنیم. با Rewire، میتوانیم این مدلها را بدون تکیه بر پارامترهای تابع به ماژولهای خود تزریق کنیم.
مفاهیم اصلی Rewire
Rewire با ارائه یک رویکرد مبتنی بر کلان برای تعریف و تزریق وابستگی ها، فرآیند DI را در Elixir ساده می کند. این یکپارچه در اکوسیستم اکسیر قرار می گیرد و کدهای تمیز و قابل نگهداری را ترویج می کند.
تزریق وابستگی با Rewire در EmailScanner
مدول
بیایید پیاده سازی کنیم EmailScanner
ماژول، که متکی بر a SpamFilterService
برای بررسی اینکه آیا یک ایمیل اسپم است یا خیر. با استفاده از Rewire به راحتی می توانیم این وابستگی را تزریق کنیم. به کد زیر دقت کنید:
defmodule EmailScanner do
def filter_email(email) do
email
|> mark_as_important()
|> SpamFilterService.check_spam()
end
defp mark_as_important(email) do
important_senders = ["boss@example.com", "hr@example.com"]
updated_email =
if Enum.any?(important_senders, fn sender -> sender == email.sender end) do
%{email | important: true}
else
email
end
updated_email
end
end
تمسخر با موکس برای تست
برای تست کردن EmailScanner
تابع فیلتر، ما می توانیم از Mox برای تمسخر استفاده کنیم SpamFilterService
مدول:
defmodule EmailScannerTest do
use ExUnit.Case
import Rewire
import Mox
rewire EmailScanner, SpamFilterService: SpamFilterServiceMock
# Ensure mocks are verified after each test
setup :verify_on_exit!
describe "filter_email/2" do
test "marks email as important from specific sender and checks for spam" do
important_sender_email = %{sender: "boss@example.com", content: "Please review the attached report.", important: false}
non_important_sender_email = %{sender: "random@example.com", content: "Check out these deals!", important: false}
# Stub the SpamFilter service to return false for all emails
stub(SpamFilterServiceMock, :check_spam, fn _email -> :false end)
# Filtering emails sent from the important sender
assert %{important: true, is_spam: false} = EmailScanner.filter_email(important_sender_email)
# Filtering emails sent from a non-important sender
assert %{important: false, is_spam: false} = EmailScanner.filter_email(non_important_sender_email)
end
end
end
بیایید آنچه را که در تست بالا اتفاق می افتد تجزیه کنیم:
در زیر کاپوت، Rewire در حال انجام چند کار جالب است. اول، درک فلسفه پشت Rewire و رویکردی که نویسنده تصمیم گرفته است را درک کنید. rewire
با استفاده از ماکروها برای ایجاد یک کپی از ماژول کار می کند. بنابراین، برای هر آزمایش، Rewire یک ماژول جدید با خردهای مشخص شده ایجاد می کند.
ایجاد یک کپی از هر ماژول به جای نادیده گرفتن ماژول اصلی به ما این امکان را می دهد که آزمایش ها را به صورت موازی بدون هیچ گونه عوارض جانبی اجرا کنیم.
مواردی که هنگام استفاده از Rewire و Mox باید در نظر بگیرید
هنگام استفاده از Rewire و Mox در پروژه های Elixir خود، موارد زیر را در نظر بگیرید:
-
سازگاری تست ناهمزمان:
Rewire به طور کامل از تست ناهمزمان باasync: true
. برخلاف نادیده گرفتن جهانی که توسط ابزارهایی مانند Meck استفاده می شود، Rewire یک کپی ماژول جداگانه برای هر آزمون ایجاد می کند. این تضمین می کند که تست ها می توانند به صورت موازی بدون تداخل با یکدیگر اجرا شوند. -
ادغام با Mox:
Rewire با تمرکز بر تزریق وابستگی بدون دیکته کردن منبع ماژول ساختگی، Mox را کاملاً تکمیل می کند. این هم افزایی امکان ادغام کارآمد و بدون درز بین این دو را فراهم می کند و آنها را به یک جفت عالی برای آزمایش اکسیر تبدیل می کند. -
تاثیر بر سرعت تست:
سیمکشی مجدد ممکن است کمی سرعت تستهای شما را کاهش دهد، اگرچه تأثیر آن معمولاً حداقل است. داده های عملکرد جامع از پایگاه های کد بزرگ هنوز در انتظار است. -
دقت پوشش تست:
بله، پوشش آزمایشی به طور دقیق با Rewire گزارش میشود و اطمینان میدهد که میتوانید به معیارهای پوشش آزمایشی خود اعتماد کنید. -
سازگاری با فرآیندهای Stateful:
Rewire با فرآیندهای حالت دار به خوبی کار می کند، مشروط بر اینکه این فرآیندها پس از سیم کشی مجدد ماژول آنها شروع شوند. برای فرآیندهایی که از قبل شروع شده اند (مانند کنترلرهای Phoenix)، Rewire ممکن است موثر نباشد زیرا سیم کشی مجدد دیگر نمی تواند اعمال شود. توصیه می شود از Rewire در درجه اول برای تست های واحدی که این محدودیت اعمال نمی شود، استفاده کنید. -
سیم کشی مجدد ماژول Erlang:
Rewire نمی تواند مستقیماً ماژول های Erlang را دوباره سیم کشی کند. با این حال، اجازه می دهد تا مراجع ماژول Erlang در ماژول های Elixir جایگزین شوند و راه حلی برای این محدودیت ارائه می دهد. -
مدیریت ماژول های تو در تو:
سیمکشی مجدد تنها وابستگیها را در ماژول سیمکشی مجدد خاص جایگزین میکند. ماژول های اطراف یا تو در تو بدون تاثیر باقی می مانند و ارجاع به ماژول های اصلی را حفظ می کنند. برای کنترل کامل، ممکن است لازم باشد این ماژول ها را به صورت جداگانه سیم کشی کنید. -
پیکربندی Formatter برای Rewire:
برای جلوگیری از افزودن پرانتز در اطراف Rewire، فرمت ترکیبی را بهروزرسانی کنید.formatter.exs
فایل باimport_deps: [:rewire]
. این تضمین می کند که نحو Rewire به درستی بدون پرانتز غیر ضروری قالب بندی شده است.
و بس!
بسته بندی
در این پست بررسی کرده ایم که چگونه Rewire و Mox می توانند به تزریق وابستگی در Elixir کمک کنند.
استفان بهنکه، خالق Rewire، با تمایل به یک راه حل ظریف تر برای تزریق وابستگی در اکسیر، به ویژه برای آزمایش واحد، انگیزه داشت. من معتقدم که او موفق شد ابزاری عالی برای جامعه اکسیر فراهم کند.
همانطور که گفته شد، Rewire یک گلوله نقره ای نیست و ممکن است ابزار مناسبی برای هر پروژه ای نباشد. ارزیابی Rewire در کنار ابزارهایی مانند Meck و تصمیم گیری بر اساس پروژه و نیازهای تیم مهم است.
کد نویسی مبارک!
PS اگر میخواهید پستهای اکسیر کیمیاگری را به محض انتشار آنها بخوانید، در خبرنامه اکسیر کیمیاگری مشترک شوید و هیچ پستی را از دست ندهید!