توسعه تست محور برای AWS CDK در پایتون
زمینه
اغلب می شنوم که مردم می گویند، ما باید TDD را با کد خود انجام دهیم. TDD خیلی بهتر است. اما TDD چیست؟ و چرا بهتر است؟ و چگونه می توان آن را به عنوان مثال CDK اضافه کرد؟ اگر می خواهید دلیل آن را بدانید، کافی است ادامه …
دانش اولیه CDK مورد نیاز است، زیرا من توضیح نمی دهم CDK چیست. اگر می خواهید، کارگاه های ارائه شده توسط AWS را دنبال کنید
- cdkworkshop.com
- cdk-advanced.workshop.aws
TDD
بنابراین TDD مخفف Test Driven Development است. Test Driven Development (TDD) یک متدولوژی توسعه نرم افزار است که بر تست نوشتن قبل از نوشتن کد واقعی تاکید دارد.
هدف اصلی TDD ایجاد یک پایگاه کد واضح، قابل اعتماد و قابل نگهداری است. این ابتدا با نوشتن تستها به دست میآید تا توسعهدهندگان بتوانند اطمینان حاصل کنند که کد آنها با مشخصات مورد نیاز مطابقت دارد و مطابق انتظار رفتار میکند.
فرآیند TDD شامل سه مرحله اصلی است که اغلب به آنها “قرمز، سبز و ریفاکتور” گفته می شود:
-
قرمز (نوشتن یک تست شکست خورده): در این مرحله، توسعه دهندگان باید قبل از پیاده سازی کد، یک تست برای یک عملکرد یا ویژگی خاص بنویسند. آزمون ابتدا باید با شکست مواجه شود، زیرا کد مربوطه هنوز نوشته نشده است.
-
سبز (کد را برای قبولی در آزمون بنویسید): سپس توسعه دهندگان حداقل کد لازم برای قبولی در آزمون را می نویسند. تمرکز بر روی موفقیت در آزمون است، نه ایجاد یک راه حل بهینه یا کامل.
-
Refactor (بهبود کد): پس از گذراندن آزمون، توسعهدهندگان میتوانند کد را اصلاح کنند تا طراحی، خوانایی و کارایی آن را بهبود بخشند و در عین حال اطمینان حاصل کنند که آزمون همچنان موفق است. این مرحله برای حفظ یک پایگاه کد تمیز و قابل نگهداری بسیار مهم است.
این مراحل به طور مکرر برای هر بخش از عملکرد تکرار می شوند، که منجر به مجموعه ای جامع از تست ها می شود که کل برنامه را پوشش می دهد.
بنابراین ممکن است بپرسید چه فوایدی دارد، خوب در چندین مورد به شما کمک می کند:
-
بهبود کیفیت کد: نوشتن تست قبل از کد واقعی، شما را تشویق میکند تا در مورد رفتار و طراحی مورد نظر به طور انتقادی فکر کنید، که منجر به کد تمیزتر و قابل اعتمادتر میشود.
-
اشکالزدایی آسانتر: هنگامی که یک تست با شکست مواجه میشود، شناسایی و رفع مشکل بسیار آسانتر است، زیرا آزمایش بر روی عملکرد خاصی متمرکز است.
-
توسعه سریعتر: با شناسایی خطاها در مراحل اولیه توسعه، TDD به جلوگیری از جلسات اشکال زدایی پرهزینه و وقت گیر در آینده کمک می کند.
-
همکاری بهتر: درک و اصلاح یک پایگاه کد خوب برای سایر اعضای تیم آسان تر است، همکاری را تسهیل می کند و احتمال معرفی اشکالات جدید را کاهش می دهد.
-
قابلیت نگهداری پیشرفته: مجموعه آزمایشی جامع به عنوان یک شبکه ایمنی عمل می کند و تضمین می کند که تغییرات آتی در پایگاه کد به طور سهوی عملکرد موجود را از بین نبرد.
بنابراین همه و همه بسیار خوب است. اما چگونه میتوانیم آن را در CDK و زیرساختهای ساخت AWS اعمال کنیم؟
سناریوی دنیای واقعی
بنابراین به عنوان یک سناریوی واقعی، میخواهم یک صف ساده و ایمن Simple Queue Service (SQS) ایجاد کنم. پیروی از بهترین شیوه های AWS. بنابراین دستورالعمل هایی که می خواهیم برای صف SQS ایجاد کنیم عبارتند از:
- صف باید از رمزگذاری KMS استفاده کند
- صف باید دارای یک صف حرف مرده باشد تا به عنوان سرریز پیام هایی عمل کند که قابل پردازش نیستند.
بعداً، میتوانیم یک تابع لامبدا اضافه کنیم که صف را میخواند و پیام را در S3 ذخیره میکند.
برو بساز
حالا چیزهای سرگرم کننده شروع می شود. بیایید با مقداردهی اولیه یک پروژه CDK جدید شروع کنیم.
➜ Hashnode mkdir secure_sqs
➜ Hashnode cd secure_sqs
➜ secure_sqs cdk init app --language=python
Applying project template app for python
# Welcome to your CDK Python project!
This is a blank project for CDK development with Python.
<...SNIPPIT...>
✅ All done!
➜ secure_sqs git:(main) source .venv/bin/activate
(.venv) ➜ secure_sqs git:(main)
کاری که من در بلوک کد بالا انجام دادم ایجاد یک پروژه CDK جدید خالی در یک دایرکتوری safe_sqs است.
با نگاهی به ساختار دایرکتوری CDK، می بینید که دایرکتوری به نام tests وجود دارد. این جایی است که ما اولین آزمون خود را اضافه خواهیم کرد.
یک تست ناموفق بنویسید (قرمز)
ما از کتابخانه اظهارات که با CDK ارائه می شود استفاده خواهیم کرد. اطلاعات بیشتر در مورد این را می توان در اسناد AWS CDK در اینجا یافت.
فایل tests/unit/test_secure_sqs_stack.py را در ویرایشگر کد مورد علاقه خود باز کنید. فایل در حال حاضر با کد زیر به صورت دیگ بخار است (به زیر مراجعه کنید). من آخرین بلوک را که در آن بررسی میکند آیا صف SQS ایجاد شده است، کامنت گذاشتهام:
import aws_cdk as core
import aws_cdk.assertions as assertions
from secure_sqs.secure_sqs_stack import SecureSqsStack
# example tests. To run these tests, uncomment this file along with the example resource in secure_sqs/secure_sqs_stack.py
def test_sqs_queue_created():
app = core.App()
stack = SecureSqsStack(app, "secure-sqs")
template = assertions.Template.from_stack(stack)
template.has_resource_properties("AWS::SQS::Queue", {
"VisibilityTimeout": 300
})
اگر تست پایتون را اجرا کنید در اینجا اتفاق میافتد این است که آزمایش سعی میکند بررسی کند که آیا الگوی CloudFormation که سنتز میشود دارای خاصیت AWS::SQS::Queue است و اینکه صف دارای بازه زمانی دید 300 ثانیه است یا خیر. از آنجایی که ما هیچ کدی ایجاد نکرده ایم، pytest باید شکست بخورد. بیایید این را تست کنیم:
(.venv) ➜ secure_sqs git:(main) pytest <aws:abn>
============================================================= test session starts =============================================================
platform darwin -- Python 3.9.16, pytest-7.2.0, pluggy-1.0.0
rootdir: /Users/yvthepief/Code/Hashnode/secure_sqs
plugins: black-0.3.12, typeguard-2.13.3, cov-4.0.0, syrupy-3.0.5
collected 1 item
tests/unit/test_secure_sqs_stack.py F [100%]
================================================================== FAILURES ===================================================================
___________________________________________________________ test_sqs_queue_created ____________________________________________________________
jsii.errors.JavaScriptError:
@jsii/kernel.RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
No matches found
<...SNIPPIT...>
=========================================================== short test summary info ===========================================================
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
============================================================== 1 failed in 3.25s ==============================================================
همانطور که می بینید آزمون با شکست مواجه شد. از آنجایی که الگو دارای 0 منبع با نوع AWS::SQS::Queue است. و این درست است، زیرا ما هنوز هیچ خط کدی ننوشته ایم. در بخش Real-World Ssenario توضیح دادیم که میخواهیم Queue رمزگذاری شود و حاوی یک صف حرف مرده باشد. بنابراین برای آن تست ایجاد کنید.
صف رمزگذاری شده KMS
از آنجایی که کتابخانه اظهارات مورد استفاده توسط CDK به دنبال مطابقت با الگوی CloudFormation است، یک رویکرد مفید می تواند با نگاه کردن به اسناد رسمی CloudFormation در منبع AWS::SQS::Queue باشد. در اینجا می توانید ببینید که برای افزودن رمزگذاری KMS به یک صف SQS، باید KmsMasterKeyId داشته باشید.
بنابراین با ایجاد یک تست شروع کنید که بررسی کند آیا صف حاوی KmsMasterKeyId است یا خیر. از آنجایی که ارزش کلید را نمی دانیم، می توانیم از کلاس Match از کتابخانه Assertions استفاده کنیم.
from aws_cdk.assertions import Match
def test_sqs_queue_is_encrypted():
app = core.App()
stack = SecureSqsStack(app, "secure-sqs")
template = assertions.Template.from_stack(stack)
template.has_resource_properties(
"AWS::SQS::Queue", {"KmsMasterKeyId": Match.any_value()}
)
این بلوک کد بالا بررسی میکند که آیا در الگوی CloudFormation، یک منبع از نوع “AWS::SQS::Queue” است و اینکه منبع با یک “KmsMasterKeyId” ایجاد شده باشد، بررسی میکند.
بیایید pytest را اجرا کنیم، که البته شکست خواهد خورد، btw من -v را برای verbose، و –tb=no را برای غیرفعال کردن traceback اضافه کردم:
(.venv) ➜ secure_sqs git:(main) ✗ pytest -v --tb=no <aws:abn>
============================================================= test session starts =============================================================
platform darwin -- Python 3.9.16, pytest-7.2.0, pluggy-1.0.0 -- /opt/homebrew/opt/[email protected]/bin/python3.9
cachedir: .pytest_cache
rootdir: /Users/yvthepief/Code/Hashnode/secure_sqs
plugins: black-0.3.12, typeguard-2.13.3, cov-4.0.0, syrupy-3.0.5
collected 2 items
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created FAILED [ 50%]
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_is_encrypted FAILED [100%]
----------------------------------------------------------- snapshot report summary -----------------------------------------------------------
=========================================================== short test summary info ===========================================================
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_is_encrypted - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
============================================================== 2 failed in 3.22s ==============================================================
اکنون 1 تست دیگر برای DeadLetterQueue اضافه کنید.
صف نامه مرده متصل به صف
در زمینه SQS، صف حرف مرده صفی است که برای ذخیره پیامهایی استفاده میشود که پس از تعداد معینی تلاش مجدد توسط صف اصلی قابل پردازش نیستند. این پیامها معمولاً پیامهای ناموفقی هستند که به دلیل مشکلاتی مانند قالببندی نادرست پیام یا استثناهای کنترل نشده در کد پردازش، fe Lambda، قابل پردازش نیستند. با استفاده از DLQ، میتوانید پیامهای مشکلساز را جدا کرده و علت اصلی آن را بررسی کنید. این می تواند به شما کمک کند مشکلات موجود در برنامه خود را شناسایی و برطرف کنید و از بروز خطاهای مشابه در آینده جلوگیری کنید. همچنین، به شما این امکان را می دهد که یک بار دیگر پیام های شکست خورده را دوباره پردازش کنید.
با نگاهی دوباره به مستندات CloudFormation “AWS::SQS::Queue”، می بینیم که باید یک RedrivePolicy با deadletterTargetArn ارائه دهید که به یک صف اشاره می کند که به عنوان یک صف حرف مرده عمل می کند. بنابراین تست را ایجاد کنید:
def test_sqs_queue_has_dead_letter_queue():
app = core.App()
stack = SecureSqsStack(app, "secure-sqs")
template = assertions.Template.from_stack(stack)
template.has_resource_properties(
"AWS::SQS::Queue", {"RedrivePolicy": {"deadLetterTargetArn": Match.any_value()}}
)
مجدداً از عبارت Match any value استفاده می کنیم زیرا در حال حاضر از صف آرن حرف مرده اطلاعی نداریم.
اجرای pytest باید اکنون در 3 تست شکست بخورد:
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_is_encrypted - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_has_dead_letter_queue - RuntimeError: Error: Template has 0 resources with type AWS::SQS::Queue.
کد قبولی در آزمون را بنویسید (سبز)
ایجاد صف SQS با CDK
همانطور که ما تست های اولیه خود را به پایان رساندیم، اکنون باید آزمون واقعی را برای قبولی در تست ها بنویسیم. بنابراین یک صف SQS رمزگذاری شده با صف حرف مرده ایجاد کنید.
فایل safe_sqs/secure_sqs_stack.py را باز کنید. این فایل یک دیگ بخار نیز دارد، اما ما قصد داریم آن را تنظیم کنیم.
from aws_cdk import (
Duration,
Stack,
aws_kms,
aws_sqs,
)
from constructs import Construct
class SecureSqsStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create key with rotation and alias
key = aws_kms.Key(
self,
"SecureQueueKmsKey",
alias="/kms/secure_queue_key",
enable_key_rotation=True,
description="Key for encrypting SQS queue",
)
# Create secure encrypted queue with
# visibility timeout of 300 seconds
queue = aws_sqs.Queue(
self,
"SecureQueue",
queue_name="secure_queue",
encryption=aws_sqs.QueueEncryption.KMS,
encryption_master_key=key,
enforce_ssl=True,
visibility_timeout=Duration.seconds(300),
)
با نگاهی به کد بالا، یک کلید مدیریت شده سفارشی KMS ایجاد می کنیم که دارای نام مستعار و چرخش کلید فعال است. این کلید برای رمزگذاری صف SQS استفاده خواهد شد. ما همچنین یک خط مشی صف تنظیم کردیم تا فقط به حمل و نقل ایمن (SSL) اجازه دهد. در نهایت، همانطور که در تستهای ایجاد شده قبلی مشخص شد، 300 ثانیه برای بازه زمانی دید تعیین کردیم.
بنابراین با ایجاد صف، بیایید نگاهی به نحوه اجرای تست ها بیندازیم:
(.venv) ➜ secure_sqs git:(main) ✗ pytest -v --tb=no
================================================================== test session starts ==================================================================
platform darwin -- Python 3.11.3, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /Users/yvthepief/Code/Hashnode/secure_sqs/.venv/bin/python3.11
cachedir: .pytest_cache
rootdir: /Users/yvthepief/Code/Hashnode/secure_sqs
plugins: typeguard-2.13.3
collected 3 items
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created PASSED [ 33%]
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_is_encrypted PASSED [ 66%]
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_has_dead_letter_queue FAILED [100%]
================================================================ short test summary info ================================================================
FAILED tests/unit/test_secure_sqs_stack.py::test_sqs_queue_has_dead_letter_queue - RuntimeError: Error: Template has 1 resources with type AWS::SQS::Q...
============================================================== 1 failed, 2 passed in 5.01s ==============================================================
2 از 3 قبلا گذشت. اما هنوز آن 100 درصدی که ما در نظر داریم نیست. برای اینکه test_sqs_queue_has_dead_letter_queue نیز پاس شود، باید یک Dead Letter Queue اضافه کنیم. صف حرف مرده را بین کلید و صف اضافه کنید و به منبع صف حرف مرده برای صف مراجعه کنید:
from aws_cdk import (
Duration,
Stack,
aws_kms,
aws_sqs,
)
from constructs import Construct
class SecureSqsStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create key with rotation and alias
key = aws_kms.Key(
self,
"SecureQueueKmsKey",
alias="/kms/secure_queue_key",
enable_key_rotation=True,
description="Key for encrypting SQS queue",
)
# Create secure encrypted dead letter queue with
# visibility timeout of 300 seconds
dead_letter_queue = aws_sqs.Queue(
self,
"SecureDeadLetterQueue",
queue_name="secure_dead_letter_queue",
encryption=aws_sqs.QueueEncryption.KMS,
encryption_master_key=key,
enforce_ssl=True,
visibility_timeout=Duration.seconds(300),
)
# Create secure encrypted queue with
# visibility timeout of 300 seconds and refer to the dlq
queue = aws_sqs.Queue(
self,
"SecureQueue",
queue_name="secure_queue",
encryption=aws_sqs.QueueEncryption.KMS,
encryption_master_key=key,
enforce_ssl=True,
visibility_timeout=Duration.seconds(300),
dead_letter_queue=aws_sqs.DeadLetterQueue(
max_receive_count=5, queue=dead_letter_queue
),
)
الان تست ها قبول میشه؟
(.venv) ➜ secure_sqs git:(main) ✗ pytest -v --tb=no
================================================================== test session starts ==================================================================
platform darwin -- Python 3.11.3, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /Users/yvthepief/Code/Hashnode/secure_sqs/.venv/bin/python3.11
cachedir: .pytest_cache
rootdir: /Users/yvthepief/Code/Hashnode/secure_sqs
plugins: typeguard-2.13.3
collected 3 items
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_created PASSED [ 33%]
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_is_encrypted PASSED [ 66%]
tests/unit/test_secure_sqs_stack.py::test_sqs_queue_has_dead_letter_queue PASSED [100%]
=================================================================== 3 passed in 4.98s ===================================================================
(.venv) ➜ secure_sqs git:(main) ✗
AWSome، آن را می گذرد! اکنون می توانیم بیشتر بسازیم.
همانطور که در مثال ها می بینید، من فقط 3 تست ایجاد کرده ام، اما با نگاه کردن به خروجی CloudFormation می توانید ببینید که 4 منبع AWS ایجاد شده است. برای سازگاری و آزمایش همه منابع، عاقلانه است که برای این منابع نیز تست هایی اضافه کنید.
گزینههای اضافی برای مثال، ایجاد یک آزمایش برای یک Lambda است که پیامهای موجود در صف امن را پردازش میکند، یا اضافه کردن آزمایشهایی برای خط مشی صف SQS. اما این همه به شما بستگی دارد! حالا برو بساز!
خلاصه
در این پست، من نشان دادم که چگونه فرآیند باید با استفاده از Test Driven Development با CDK کار کند. از کوچک شروع کنید و قدم به قدم. عاقلانه نیست که برای تمام منابعی که در برنامه هدف نهایی خود ایجاد خواهید کرد، آزمایش ایجاد کنید. این فقط تست های شما را به هم می ریزد. بنابراین مهمترین چیز این است که با تست ها شروع کنید و یک بار دیگر این تست ها را کوچک نگه دارید. در اینجا تکرار مهمترین عامل است!