Test platform series (73) design test plan function

Hello ~ I'm Milo!

I'm building an open source interface testing platform from 0 to 1, and I'm also writing a complete set of corresponding tutorials. I hope you can support me.

Welcome to my official account test development pit, get the latest article tutorial!

review

In the previous section, we briefly introduced the next APScheduler. In this section, we will write relevant contents of the test plan.

Design test schedule

In fact, test plan can also be called test set. It is a set of use cases. And has corresponding characteristics:

  • Timed execution

  • Notification method after execution

  • What is the passing rate lower than? Send mail / nail and other notices

  • priority

  • How many case s are covered

  • case failure, retry interval, etc

    What may be strange here is that the test plan + test set are coupled together.

    Based on the above ideas, we can design the test plan table. Refer to the notes for the meaning of specific fields.

    (the design of the data table is not necessarily perfect, and it will be expanded according to business requirements in the future)

from sqlalchemy import Column, String, TEXT, UniqueConstraint, BOOLEAN, SMALLINT, INT
from sqlalchemy.dialects.mysql import TINYTEXT

from app.models.basic import PityBase


class PityTestPlan(PityBase):
    project_id = Column(INT, nullable=False)
    # Test plan execution environment; multiple choices are allowed
    env = Column(String(64), nullable=False)
    # Test plan name
    name = Column(String(32), nullable=False)
    # Test plan priority
    priority = Column(String(3), nullable=False)
    # cron expressions 
    cron = Column(String(12), nullable=False)
    # Use case list
    case_list = Column(TEXT, nullable=False)
    # Parallel / serial (sequential execution or not)
    ordered = Column(BOOLEAN, default=False)
    # If the pass rate is lower than this number, the notification will be sent automatically
    pass_rate = Column(SMALLINT, default=80)
    # Inform the user that there is only mailbox at present. The mobile phone number field may be improved in the subsequent user table for notification
    receiver = Column(TEXT)
    # Notification method 0: email 1: nailing 2: enterprise wechat 3: Flying book supports multiple choices
    msg_type = Column(TINYTEXT)
    # Retry interval for single case failure: 2 minutes by default
    retry_minutes = Column(SMALLINT, default=2)
    # Is the test plan in progress
    state = Column(SMALLINT, default=0, comment="0: Not started 1: In operation")

    __table_args__ = (
        UniqueConstraint('project_id', 'name', 'deleted_at'),
    )

    __tablename__ = "pity_test_plan"

    def __init__(self, project_id, env, case_list, name, priority, cron, ordered, pass_rate, receiver, msg_type,
                 retry_minutes, user, state=0, id=None):
        super().__init__(user, id)
        self.env = ",".join(map(str, env))
        self.case_list = ",".join(map(str, case_list))
        self.name = name
        self.project_id = project_id
        self.priority = priority
        self.ordered = ordered
        self.cron = cron
        self.pass_rate = pass_rate
        self.receiver = ",".join(map(str, receiver))
        self.msg_type = ",".join(map(str, msg_type))
        self.retry_minutes = retry_minutes
        self.state = state

It is worth noting here that the FORM data we defined include environment list env, receiver, and use case list case_list is an array. We need a wave of conversion.

Write CRUD method

  • Extract asynchronous paging method and where method

Add a pagination method in the DatabaseHelper class, accept page and size, session and sql parameters, read the total number of sql matches first, if it is 0, return directly, otherwise obtain the data of the corresponding page through offset and limit.

The where method is used to improve our usual multi condition query, similar to this:

  • Write new test plan method

You can see that after the transformation, we only need to call the where method and do not need to write if name= '': such a statement.

Because we do not allow test plans with the same name in the same project, the condition is that the project id+name cannot be repeated.

  • Preparation of addition, modification and deletion methods
from sqlalchemy import select

from app.models import async_session, DatabaseHelper
from app.models.schema.test_plan import PityTestPlanForm
from app.models.test_plan import PityTestPlan
from app.utils.logger import Log


class PityTestPlanDao(object):
    log = Log("PityTestPlanDao")

    @staticmethod
    async def list_test_plan(page: int, size: int, project_id: int = None, name: str = ''):
        try:
            async with async_session() as session:
                conditions = [PityTestPlan.deleted_at == 0]
                DatabaseHelper.where(project_id, PityTestPlan.project_id == project_id, conditions) \
                    .where(name, PityTestPlan.name.like(f"%{name}%"), conditions)
                sql = select(PityTestPlan).where(conditions)
                result, total = await DatabaseHelper.pagination(page, size, session, sql)
                return result, total
        except Exception as e:
            PityTestPlanDao.log.error(f"Failed to get test plan: {str(e)}")
            raise Exception(f"Failed to get test plan: {str(e)}")

    @staticmethod
    async def insert_test_plan(plan: PityTestPlanForm, user: int):
        try:
            async with async_session() as session:
                async with session.begin():
                    query = await session.execute(select(PityTestPlan).where(PityTestPlan.project_id == plan.project_id,
                                                                             PityTestPlan.name == plan.name,
                                                                             PityTestPlan.deleted_at == 0))
                    if query.scalars().first() is not None:
                        raise Exception("Test plan already exists")
                    plan = PityTestPlan(**plan.dict(), user=user)
                    await session.add(plan)
        except Exception as e:
            PityTestPlanDao.log.error(f"Failed to add test plan: {str(e)}")
            raise Exception(f"Add failed: {str(e)}")

    @staticmethod
    async def update_test_plan(plan: PityTestPlanForm, user: int):
        try:
            async with async_session() as session:
                async with session.begin():
                    query = await session.execute(
                        select(PityTestPlan).where(PityTestPlan.id == plan.id, PityTestPlan.deleted_at == 0))
                    data = query.scalars().first()
                    if data is None:
                        raise Exception("Test plan does not exist")
                    DatabaseHelper.update_model(data, plan, user)
        except Exception as e:
            PityTestPlanDao.log.error(f"Failed to edit test plan: {str(e)}")
            raise Exception(f"Edit failed: {str(e)}")

    @staticmethod
    async def delete_test_plan(id: int, user: int):
        try:
            async with async_session() as session:
                async with session.begin():
                    query = await session.execute(
                        select(PityTestPlan).where(PityTestPlan.id == plan.id, PityTestPlan.deleted_at == 0))
                    data = query.scalars().first()
                    if data is None:
                        raise Exception("Test plan does not exist")
                    DatabaseHelper.delete_model(data, user)
        except Exception as e:
            PityTestPlanDao.log.error(f"Failed to delete test plan: {str(e)}")
            raise Exception(f"Deletion failed: {str(e)}")
            
    @staticmethod
    async def query_test_plan(id: int) -> PityTestPlan:
        try:
            async with async_session() as session:
                sql = select(PityTestPlan).where(PityTestPlan.deleted_at == 0, PityTestPlan.id == id)
                data = await session.execute(sql)
                return data.scalars().first()
        except Exception as e:
            PityTestPlanDao.log.error(f"Failed to get test plan: {str(e)}")
            raise Exception(f"Failed to get test plan: {str(e)}")

The basic idea is almost the same, old CRUD! I won't say much here. Those who don't understand the async content of sqlalchemy can go to the official website to see the demo.

This query method is used to query test plan data for scheduled tasks. Due to soft deletion, you often forget to bring deleted_at==0.

Write relevant interfaces (APP / routes / testcase / testplan. Py)

from fastapi import Depends

from app.dao.test_case.TestPlan import PityTestPlanDao
from app.handler.fatcory import PityResponse
from app.models.schema.test_plan import PityTestPlanForm
from app.routers import Permission
from app.routers.testcase.testcase import router
from config import Config


@router.get("/plan/list")
async def list_test_plan(page: int, size: int, project_id: int = None, name: str = "", user_info=Depends(Permission())):
    try:
        data, total = await PityTestPlanDao.list_test_plan(page, size, project_id, name)
        return PityResponse.success_with_size(PityResponse.model_to_list(data), total=total)
    except Exception as e:
        return PityResponse.failed(str(e))


@router.get("/plan/insert")
async def insert_test_plan(form: PityTestPlanForm, user_info=Depends(Permission(Config.MANAGER))):
    try:
        await PityTestPlanDao.insert_test_plan(form, user_info['id'])
        return PityResponse.success()
    except Exception as e:
        return PityResponse.failed(str(e))


@router.get("/plan/update")
async def update_test_plan(form: PityTestPlanForm, user_info=Depends(Permission(Config.MANAGER))):
    try:
        await PityTestPlanDao.update_test_plan(form, user_info['id'])
        return PityResponse.success()
    except Exception as e:
        return PityResponse.failed(str(e))


@router.get("/plan/delete")
async def delete_test_plan(id: int, user_info=Depends(Permission(Config.MANAGER))):
    try:
        await PityTestPlanDao.delete_test_plan(id, user_info['id'])
        return PityResponse.success()
    except Exception as e:
        return PityResponse.failed(str(e))

Here, we give the MANAGER the authority to add, delete and modify the test plan, but it's better to give it to the corresponding project MANAGER, but that will be a little more complicated. Let's be lazy for the time being and maybe improve it.

Today's content is shared here. The next section will introduce how to combine the test plan with the APScheduler, and then write the front page of the test plan to complete the scheduled task function.

Tags: Python React Testing FastAPI

Posted on Sat, 06 Nov 2021 23:48:18 -0400 by locell