برنامه نویسی

اجرای آزمایش خودکار پایان به پایان: استفاده از برنامه CI/CD خود با برنامه Cloud Development Clit (CDK)

من اخیراً پروژه جدیدی را با محوریت مدیریت ورودی خود آغاز کردم. من برای یک کارگزار بیمه آلمانی کار می کنم که هدف آن مدرن سازی رسیدگی به نامه های فیزیکی است. متأسفانه ، در بخش بیمه آلمان ، ما نمی توانیم از API یا پیاده سازی های فنی مشابه استفاده کنیم. درعوض ، مدیران حساب ما با خواندن آنها ، استخراج اطلاعات و در نهایت وارد کردن کلیه جزئیات مربوطه در CRM ما با نامه ها سر و کار دارند.

حجم نامه های فیزیکی که من به آنها اشاره می کنم تقریباً 8،500 صفحه در ماه است. بنابراین ، اطمینان از کیفیت معماری مبتنی بر رویداد ما بسیار مهم است. در این مقاله ، من نشان خواهم داد که چگونه ما یک رویکرد آزمایش پایان (E2E) را در خط لوله CI/CD خود با استفاده از کیت توسعه ابر (CDK) برای ایجاد زیرساخت ها اجرا کردیم.

معماری

من وقت زیادی را برای بحث در مورد معماری نمی گذرانم ، زیرا ممکن است در آینده پست وبلاگ دیگری در مورد این مورد استفاده بنویسم. با این حال ، من حداقل اطلاعات لازم را برای ارائه سرنخ به شما ارائه می دهم. در اصل ، شکل 1 همه چیز را نشان می دهد. ما یک اسکنر پیش فرض داریم که تمام اسکن ها را به سرویس ایمیل ساده (SES) منتقل می کند. هر نامه به عنوان ایمیل برای SES رفتار می شود. SES تمام ایمیل ها را به عنوان اشیاء MIME در S3 ذخیره می کند ، و یک عملکرد Lambda پیوست PDF را بازیابی می کند ، متعاقباً توابع مرحله AWS را برای پردازش PDF ایجاد می کند. با استفاده از AWS Textract ، ما نتایج OCR را بدست می آوریم و از آمازون از بستر آمازون برای انجام پردازش اسناد هوشمند (IDP) استفاده می کنیم. از این IDP ، ما اطلاعات خاصی را برای ایجاد نام جدید (می دانم ، مدرسه قدیمی!) استخراج می کنیم. سرانجام ، یک تابع Lambda PDF پردازش شده را به سهم فایل SMB سوق می دهد.

شکل 1: نشان دادن معماری ساده از سیستم مدیریت ورودی

اجرای یک استراتژی آزمون: آزمایشات آزمایشی E2E

من تمام پست های وبلاگ را در مورد طراحی موارد آزمون مجدداً تکرار نمی کنم ، اما دو جنبه اساسی را برجسته خواهم کرد. اول ، ما باید یک سلسله مراتب آزمون ایجاد کنیم. تست های واحد لایه پایه را تشکیل می دهند ، تست های ادغام لایه میانی هستند ، در حالی که تست های E2E لایه بالایی را در هر مورد اجرای آزمون نشان می دهد. تست های واحد ارزیابی می کنند که آیا یک ورودی خاص خروجی مورد انتظار را از یک عملکرد تولید می کند. آزمون های ادغام ارزیابی می کنند که آیا یک مؤلفه می تواند نتایج حاصل از یک مؤلفه دوم را ایجاد و کنترل کند. آزمون های E2E ارزیابی می کنند که آیا همه مؤلفه های یک سیستم مطابق آنچه در نظر گرفته شده با هم کار می کنند یا خیر. در حالی که امیدوارم همه از تست های واحد برای تضمین کیفیت لامبدها و سایر مؤلفه های کد استفاده کنند ، تست های ادغام و تست های E2E به طور قابل توجهی پیچیده تر و پرهزینه تر هستند.

بیایید یک قدم عقب برداریم و آنچه را که معماری توصیف شده برای روشن شدن پیچیدگی آزمایش E2E انجام می دهد ، تجزیه و تحلیل کنیم. ابتدا باید یک ایمیل پردازش کنیم. در مرحله بعد ، ما PDF را پردازش می کنیم ، و در آخر ، پرونده PDF پردازش شده را به یک پوشه خاص در محیط ابر خود سوق می دهیم. از این انتزاع ، تست های E2E ما نیاز به شبیه سازی ارسال ایمیل برای مسخره کردن اسکنر دارند. ثانیا ، ما باید وقایع ایجاد شده توسط SES را ردیابی کنیم. اگر می خواهید رویداد خاص و عملکرد Lambda را که شیء MIME را پردازش می کند ، می تواند مشکل باشد. در عوض ، من از عملکرد مرحله AWS خود استفاده کردم. لیست اعدام های این عملکرد مرحله ایده خوبی به نظر می رسید. سرانجام ، من می توانم برای جستجوی پرونده آزمایشی PDF ، به اشتراک گذاری پرونده SMB خود نگاه کنم.

پیش نیازهای این رویکرد

شما ممکن است قبلاً فرضیاتی را که باید برای آنها ملاقات کنیم در نظر بگیرید ، درست است؟ اگر خط لوله CI/CD شما در یک بخش شبکه ای اجرا نمی شود که می تواند به اشتراک پرونده SMB منتقل شود ، شما از شانس خارج می شوید. بنابراین ، ادغام شبکه می تواند بسیار مهم باشد. علاوه بر این ، در نظر گرفتن مجوزهای IAM ضروری است. ما از دستور cdk bootstrap برای ایجاد کلیه مؤلفه های مربوطه برای استقرار استفاده می کنیم. با این حال ، این مجموعه از منابع اجازه لیست و توصیف منابع غیر مرتبط با CDK را نمی دهد. بنابراین ، بخشی از برنامه شما باید یک نقش IAM باشد که توسط سرور CI/CD شما قابل فرض است. در نتیجه ، شما باید مجموعه CDK Bootstrap را تغییر دهید تا به شما امکان دهد نقش جدید IAM خود را برای اهداف آزمایش فرض کنید.

سرانجام ، از آنجا که CDK از نامگذاری منابع پویا پشتیبانی می کند ، که شما باید از آن استفاده کنید ، ما نمی توانیم به طرح های نامگذاری قطعی اعتماد کنیم. راه حل در معرض نمایش ایمیل ، عملکرد مرحله و نقش IAM برای آزمایش از طریق خروجی های CloudFormation است. هر خروجی دارای نام صادراتی قابل خواندن و مداوم خواهد بود و به ما امکان می دهد نام منابع پویا را بازیابی کنیم.

سرانجام ، ما برای استفاده از هر دو ، باید اشخاص ایمیل و دامنه خود را در SES تأیید کنیم. برای ایمیل ها ، باید روی یک پیوند کلیک کنید و برای دامنه ها ، باید سوابق خود را به سرویس دامنه خود اضافه کنید ، خواه مسیر 53 باشد یا یک ارائه دهنده دیگر.

بحث در مورد این رویکرد

بیایید در مورد نکات قبلی بحث کنیم. اول ، اسکنر به تصویر کشیده شده در شکل در این سناریوی آزمایش E2E استفاده نمی شود. نکته عادلانه سوال من این است: آیا تلاش برای اجرای خود اسکنر واقعاً ارزیابی معماری AWS را توجیه می کند؟ اسکنر ممکن است به طور جداگانه از محیط شما واقع شود. در نهایت ، شما درست هستید ؛ این کاملاً E2E نیست ، اما ما کاملاً نزدیک هستیم.

نکته دیگری که می خواهم به آن اشاره کنم این است که این آزمون E2E از موارد تست عالی استفاده می کند. این بدان معنی است که ما از PDF هایی استفاده می کنیم که می توانند به طور مؤثر توسط Textract و Bedrock ارزیابی و پردازش شوند ، و اطمینان حاصل می کنم که می دانم پرونده نهایی چگونه به نظر می رسد. از دیدگاه من ، شما می توانید از این امر نیز انتقاد کنید ، اما جوهر آزمایش E2E در مورد ارزیابی کیفیت از نظر حجم زیاد نیست. این در مورد پیکربندی صحیح سیستم برای دستیابی به نتایج قابل پیش بینی است. عملکرد کلی سیستم در تولید کنترل می شود.

من همچنین می شنوم که شما می گویید قسمت های خاصی از معماری در استقرار اولیه مانند موجودات SES کاملاً خودکار نیستند. شما کاملاً درست هستید. در SES ، ما منابع مستقلی مانند این قاعده ایجاد می کنیم تا ایمیل های ورودی را به S3 و Lambda ارسال کنیم ، و همچنین منابع وابسته مانند دامنه و اشخاص ایمیل ، که باید تأیید یا ایجاد و بازیابی شوند ، مانند اعتبار SMTP. گفته می شود ، تلاش دستی درگیر است ، اما این یک کار یک بار است. همانطور که قبلاً نیز اشاره کردم ، حرکت به سمت آزمایش E2E در مقایسه با آزمایش واحد ، نیاز به زمان و مداخله دستی بیشتری دارد.

اجرای فیلمنامه

بیایید به اسکریپت ها شیرجه بزنیم. اول ، در اینجا نقش IAM و نمونه ای برای خروجی CloudFormation آورده شده است:

from aws_cdk import aws_iam as iam, CfnOutput
from constructs import Construct

class IAMRoleStack(Stack):
    """Create the IAM deployment."""

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        **kwargs,
    ) -> None:
        """Create the IAM deployment.

        Args:
            scope (Construct): CDK App scope
        """
        role = iam.Role(
            self,
            id="TestIAMRole",
            assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
            description="IAM Role for E2E testing",
        )

        role.add_to_policy(
            iam.PolicyStatement(
                actions=[
                    "states:ListExecutions",
                    "states:DescribeExecution",
                ],
                resources=["*"],
            )
        )

        CfnOutput(
            self.scope,
            id="RoleArn",
            value=role.role_arn,
            export_name="E2eTestIAMRole",
            description="ARN of the created IAM Role",
        )
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در مرحله بعد ، اجرای Bash برای خط لوله CI/CD ما ، در مورد من با استفاده از Azure DevOps اما می تواند هر ابزار CI/CD باشد:

export AWS_DEFAULT_REGION=${{ parameters.AwsRegion }}
KST=$( aws sts assume-role --endpoint-url https://sts.${{ parameters.AwsRegion }}.amazonaws.com --role-arn arn:aws:iam::${{ parameters.AccountID }}:role/${{ parameters.CDKDeploymentRole }}-${{ parameters.AccountID }}-${{ parameters.AwsRegion }} --role-session-name $(Build.SourceVersion) --duration-seconds 3600)
export AWS_ACCESS_KEY_ID=$(echo $KST | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $KST | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $KST | jq -r .Credentials.SessionToken)
export EMAIL_PROCESSOR=$(aws cloudformation describe-stacks --stack-name mrht-developer-posteingang-stack --query "Stacks[0].Outputs[?ExportName=='ExportEmailProcessor'].OutputValue" --output text)
export STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name mrht-developer-posteingang-stack --query "Stacks[0].Outputs[?ExportName=='ExportStateMachine'].OutputValue" --output text)
export IAM_ROLE_ARN=$(aws cloudformation describe-stacks --stack-name mrht-developer-posteingang-stack --query "Stacks[0].Outputs[?ExportName=='E2eTestIAMRole'].OutputValue" --output text)

KST=$( aws sts assume-role --endpoint-url https://sts.${{ parameters.AwsRegion }}.amazonaws.com --role-arn $IAM_ROLE_ARN --role-session-name $(Build.SourceVersion) --duration-seconds 3600)
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
export AWS_ACCESS_KEY_ID=$(echo $KST | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $KST | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $KST | jq -r .Credentials.SessionToken)
python3 src/e2e_test/e2e_test.py
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اولی aws sts assume-role فرمان به ما کمک می کند تا خروجی های CloudFormation را بازیابی کنیم. مجوزهای لازم برای این امر بخشی از مجموعه CDK Bootstrap است. دوم aws sts assume-role فرمان ما را قادر می سازد تا آزمایش را آغاز کنیم.

حال ، بیایید نگاهی به پرونده e2e_test.py بیندازیم:

import os
import time
import logging
import boto3
from botocore.exceptions import ClientError, ParamValidationError
import smbclient
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

class SMBClient:
    """Manage an SMB connection to access shared files."""

    def __init__(self, username: str, password: str, servername: str):
        """Initialize the SMBClient with connection details.

        Args:
            username (str): The username for the SMB connection.
            password (str): The password for the SMB connection.
            servername (str): The hostname or IP address of the SMB server.
        """
        self.username = username
        self.password = password
        self.port = 445
        self.host_name = servername

    def connect_smb(self, smb_client: smbclient) -> None:
        """Connect to the SMB server.

        Args:
            smb_client (smbclient): The SMB client instance.
        """
        logger.info(f"Trying to connect to {self.host_name} as {self.username}.")
        try:
            smb_client.register_session(
                server=self.host_name, username=self.username, password=self.password
            )
            logger.info("Session established successfully.")
        except Exception as e:
            logger.exception(f"Failed to connect to SMB share: {e}")
            raise
        return True

    def list_directory(self, path: str, smb_client: smbclient) -> None:
        """List the contents of an SMB directory.

        Args:
            path (str): The path to the directory.
            smb_client (smbclient): The SMB client instance.

        Returns:
            List[str]: A list of the directory contents, or an empty list if an error occurs.
        """
        entries = []
        try:
            for entry in smb_client.listdir(path):
                logger.info(f"Found entry: {entry}")
                entries.append(entry)
            return entries
        except Exception as e:
            logger.exception(f"Error listing directory: {e}")
            return []

def send_email(
    sender: str,
    recipient: str,
    subject: str,
    body_path: str,
    attachment_path: str,
    smtp_server: str,
    smtp_port: int,
    smtp_username: str,
    smtp_password: str,
):
    """Send an email with a PDF attachment using SMTP.

    Args:
        sender (str): The email address of the sender.
        recipient (str): The email address of the recipient.
        subject (str): The subject of the email.
        body_path (str): The path to the text file containing the body of the email.
        attachment_path (str): The path to the PDF file to be attached.
        smtp_server (str): The SMTP server address.
        smtp_port (int): The SMTP server port.
        smtp_username (str): The SMTP username.
        smtp_password (str): The SMTP password.
    """
    msg = MIMEMultipart()
    msg["Subject"] = subject
    msg["From"] = sender
    msg["To"] = recipient

    with open(body_path, "r", encoding="utf-8") as text_file:
        text = MIMEText(text_file.read())
        msg.attach(text)

    with open(attachment_path, "rb") as pdf_file:
        attachment = MIMEApplication(pdf_file.read(), _subtype="pdf")
        attachment.add_header(
            "Content-Disposition", "attachment", filename="attachment.pdf"
        )
        msg.attach(attachment)

    try:
        with smtplib.SMTP(smtp_server, smtp_port) as s:
            s.starttls()
            s.login(smtp_username, smtp_password)
            s.send_message(msg)
            logger.info("Email sent successfully.")
    except Exception as e:
        logger.error(f"Error sending email: {str(e)}")
        raise


def main():
    """Run E2E test."""
    try:
        state_machine_arn = os.getenv("STATE_MACHINE_ARN")
        stage = os.getenv("STAGE")
        smb_user = os.getenv("SMB_USERNAME")
        smb_password = os.getenv("SMB_PASSWORD")
        smb_host_ip = os.getenv("SMB_SERVER")
        smb_path = f"{smb_host_ip}\\folder$\\folder\\"
        smtp_server = os.getenv("SMTP_SERVER")
        smtp_port = int(os.getenv("SMTP_PORT"))
        smtp_username = os.getenv("SMTP_USERNAME")
        smtp_password = os.getenv("SMTP_PASSWORD")
    except KeyError as e:
        logger.exception(e)
        raise

    try:
        sender = "scan2mail@example.com"
        recipient = "scan2mail@example.com"
        subject = "Test Email with Attachment"
        body_path = "./src/e2e_test/email_body.txt"
        attachment_path = "./src/e2e_test/test.pdf"

        send_email(
            sender=sender,
            recipient=recipient,
            subject=subject,
            body_path=body_path,
            attachment_path=attachment_path,
            smtp_server=smtp_server,
            smtp_port=smtp_port,
            smtp_username=smtp_username,
            smtp_password=smtp_password,
        )

        time.sleep(15)

        stepfunctions_client = boto3.client("stepfunctions")

        list_executions_response = stepfunctions_client.list_executions(
            stateMachineArn=state_machine_arn, maxResults=1, statusFilter="RUNNING"
        )

        if list_executions_response["executions"]:
            execution_arn = list_executions_response["executions"][0]["executionArn"]
            logger.info(f"Found execution: {execution_arn}")

            while True:
                response = stepfunctions_client.describe_execution(
                    executionArn=execution_arn
                )
                status = response["status"]

                if status != "RUNNING":
                    logger.info(f"Step Function execution status: {status}")
                    break

                logger.info(
                    "Step Function is still running. Checking again in 20 seconds..."
                )
                time.sleep(20)
        else:
            logger.warning("No running execution found.")
            raise

    except (ClientError, ParamValidationError) as e:
        logger.error(f"An error occurred: {str(e)}")
        raise

    counter = 0
    try:
        smb = SMBClient(
            username=smb_user,
            password=smb_password,
            servername=smb_host_ip,
        )
        smb.connect_smb(smbclient)
        response = smb.list_directory(smb_path, smbclient)
        desired_contract_id = "__102865946013"
        for file_name in response:
            if desired_contract_id in file_name:
                logger.info(f"Found the ID '{desired_contract_id}' in the directory contents.")
                counter += 1
    except Exception as e:
        logger.exception(e)
        raise

    if counter == 0:
        raise ValueError("Target ID not found")

    logger.info("Finished E2E test")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        logger.error(f"An error occurred: {str(e)}")
        raise
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

این اسکریپت به عنوان آخرین مرحله از خط لوله CI/CD من پس از استقرار موفقیت آمیز CDK اجرا می شود. ابتدا همه متغیرهای مربوطه را از محیط خط لوله CI/CD جمع می کنیم. بدیهی است ، شما همچنین می توانید از خدمات AWS مانند فروشگاه پارامتر Simple Systems Manager (SSM) یا مدیر AWS Secrets استفاده کنید. با این کار ، ما یک آزمایش را از مخزن و همچنین یک بدنه آزمایش برای ایمیل می گیریم. اگر از چندین مورد استفاده می کنید ، احتمالاً ایده بهتری برای اجرای یک فروشگاه مورد آزمایش در S3 است.
با استفاده از اعتبارنامه ها و مورد آزمون ، می توانیم ایمیلی ایجاد کنیم که به آدرس ایمیل مورد نظر ما ارسال شود. در این حالت ، فرستنده و گیرنده یکسان هستند. پس از ارسال ایمیل با موفقیت ، ما مدتی صبر می کنیم و سپس به لیست و توصیف عملکرد مرحله خود همانطور که در شکل 1 نشان داده شده است ، ادامه می دهیم. اگر عملکرد مرحله با موفقیت تکمیل شود ، اسکریپت فهرست فهرست هدف SMB را لیست می کند و شناسه قرارداد مورد نظر ما را از پرونده آزمون PDF جستجو می کند. سرانجام ، آزمون را از فهرست SMB تمیز می کنیم.

افکار نهایی

مفهوم و اجرای آزمون های E2E بدون شک نیاز به تلاش قابل توجهی دارد ، و من کاملاً موافقم که این یک نمونه نسبتاً ساده است. با این حال ، اتوماسیون به اطمینان از کیفیت کمک می کند و باعث صرفه جویی در زمان قابل توجهی می شود.

فرآیند دستی چیزی شبیه به این خواهد بود: ارسال ایمیل با استفاده از مشتری مورد علاقه خود ، به این امید که شما از مورد آزمایش صحیح غافل نشوید ، منتظر ظاهر شدن پرونده در فروشگاه پرونده SMB هستید و بررسی می کنید که آیا شناسه قرارداد با پرونده تست شما مطابقت دارد یا خیر. این رویکرد دستی مطمئناً طولانی تر از روش خودکار مورد بحث است که حدود 1 دقیقه و 30 ثانیه طول می کشد.

نظر شما چیست؟ آیا من چیزها را درست کردم؟ نظر شما چیست؟ خوشحالم که بیشتر بحث کردم و به هر سؤالی که ممکن است داشته باشید پاسخ می دهم.

برنامه نویسی مبارک :-)!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا