测试平台系列(116) 设计地址管理功能.md

2022/6/7 测试平台接口测试FastApiPythonReact

highlight: atom-one-dark theme: vuepress--- 一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情 (opens new window)

大家好~我是米洛
我正在从0到1打造一个开源的接口测试平台, 也在编写一套与之对应的教程,希望大家多多支持。
欢迎关注我的公众号米洛的测开日记,一起交流学习!

# 回顾

上一节我们讲了如何用Github Action帮助我们部署自己的项目,这一节我们来改善一下我们现有的请求方式。先来看看当前项目的使用情况:

在请求地址栏,我们可以看到,这边输入的仍然是一个完整的地址,这样会显得我们很呆,我们来思考下这里的优化点:

很多公司的不同业务模块的地址前缀都是固定的,比如https://api.xxx.com,亦或者是https://xxx.com/user。如果我们每次都用全局变量去做这个事情,会表现得很繁琐。如果给你一些地址的选项: 大后台系统xxx服务这种更直观的方式,并且能够更显眼地看到对应的url,那样使用起来体验更佳。针对全局变量,我们对于多个环境的处理虽然也支持,但是并不特别友好。

所以我们可以尝试把环境地址这些数据从url中剥离出来,让url = 网关 + 路由,使得一个http的请求地址更易理解。

# 编写app/models/address.py

"""
Python请求网关地址表
"""

from sqlalchemy import Column, INT, String, UniqueConstraint

from app.models.basic import PityBase


class PityGateway(PityBase):
    __tablename__ = 'pity_gateway'
    __table_args__ = (
        UniqueConstraint('env', 'name', 'deleted_at'),
    )
    env = Column(INT, comment='对应环境')
    name = Column(String(32), comment="网关名称")
    gateway = Column(String(128), comment="网关地址")

    __fields__ = (name, env, gateway)
    __tag__ = "网关"
    __alias__ = dict(name="网关名称", env="环境", gateway="网关地址")
    __show__ = 3

    def __init__(self, env, name, gateway, user, id=None):
        super().__init__(user, id)
        self.name = name
        self.env = env
        self.gateway = gateway

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

我们的表数据很简单,基本就是以下3个重点:

  • 对应环境
  • 名称
  • 对应的url前缀地址,也可以叫gateway

# 编写app/crud/config/AddressDao.py

from app.crud import Mapper
from app.models.address import PityGateway
from app.utils.decorator import dao
from app.utils.logger import Log


@dao(PityGateway, Log("PityRedisConfigDao"))
class PityGatewayDao(Mapper):
    pass

1
2
3
4
5
6
7
8
9
10

有了我们编写好的基础Mapper,写一个crud接口就很简单了。

# 编写app/routers/config/address.py

from fastapi import Depends

from app.crud.config.AddressDao import PityGatewayDao
from app.handler.fatcory import PityResponse
from app.models.address import PityGateway
from app.routers import Permission, get_session
from app.routers.config.environment import router
from app.schema.address import PityAddressForm
from config import Config


@router.get("/gateway/list", summary="查询网关地址")
async def list_gateway(name: str = '', gateway: str = '', env: int = None,
                       user_info=Depends(Permission(Config.MEMBER))):
    data = await PityGatewayDao.list_record(env=env, gateway=f"%{gateway}%", name=f"%{name}%")
    return PityResponse.success(PityResponse.model_to_list(data))


@router.post("/gateway/insert", summary="添加网关地址", description="添加网关地址,只有组长可以操作")
async def insert_gateway(form: PityAddressForm, user_info=Depends(Permission(Config.MANAGER))):
    model = PityGateway(**form.dict(), user=user_info['id'])
    model = await PityGatewayDao.insert_record(model, True)
    return PityResponse.success(model)


@router.post("/gateway/update", summary="编辑网关地址", description="编辑网关地址,只有组长可以操作")
async def insert_gateway(form: PityAddressForm, user_info=Depends(Permission(Config.MANAGER))):
    model = await PityGatewayDao.update_record_by_id(user_info['id'], form, True, log=True)
    return PityResponse.success(model)


@router.get("/gateway/delete", summary="删除网关地址", description="根据id删除网关地址,只有组长可以操作")
async def delete_gateway(id: int, user_info=Depends(Permission(Config.MANAGER)), session=Depends(get_session)):
    await PityGatewayDao.delete_record_by_id(session, user_info['id'], id)
    return PityResponse.success()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

可以看到router里面的内容十分简单,基本上调用一下curd里面的方法即可。这里重点讲一下查询那里:

查询接口的话,我们的name/gateway都需要支持模糊查找,虽然我们知道前后匹配的效率很低,不会用到索引去查找对应的数据,但考虑到地址数据不会特别多,所以我们采用更友好的查询方式。

这里给gateway和name加上%%,在调用list_record接口时,会自动被查询条件识别为like,如果为空则不会给出任何查询条件(即查询所有数据)。

# 编写前端页面

基本上写到这个样子就差不多啦,具体的代码还是老样子:

import React, {memo, useEffect, useState} from 'react';
import {PageContainer} from "@ant-design/pro-layout";
import {connect} from "umi";
import {Button, Card, Col, Divider, Form, Input, Row, Select, Table} from "antd";
import {CONFIG} from "@/consts/config";
import TooltipTextIcon from "@/components/Icon/TooltipTextIcon";
import {PlusOutlined} from "@ant-design/icons";
import FormForModal from "@/components/PityForm/FormForModal";
import PityPopConfirm from "@/components/Confirm/PityPopConfirm";

const {Option} = Select;

const Address = ({loading, gconfig, dispatch}) => {

  const [form] = Form.useForm();
  const {envList, envMap, addressList} = gconfig;
  const [modal, setModal] = useState(false);
  const [item, setItem] = useState({});

  const fetchEnvList = () => {
    dispatch({
      type: 'gconfig/fetchEnvList',
      payload: {
        page: 1,
        size: 1000,
        exactly: true
      }
    })
  }

  const fetchAddress = () => {
    const values = form.getFieldsValue()
    console.log(values)
    dispatch({
      type: 'gconfig/fetchAddress',
      payload: values,
    })
  }

  const isLoading = loading.effects['gconfig/fetchAddress'] || loading.effects['gconfig/fetchEnvList'] || loading.effects['gconfig/updateAddress']
    || loading.effects['gconfig/insertAddress'] || loading.effects['gconfig/deleteAddress']

  useEffect(() => {
    fetchEnvList()
    fetchAddress()
  }, []);

  const columns = [
    {
      title: '环境',
      key: 'env',
      dataIndex: 'env',
      render: env => envMap[env],
    },
    {
      title: '名称',
      key: 'name',
      dataIndex: 'name',
    },
    {
      title: <TooltipTextIcon title="地址一般是服务的基础地址,比如https://api.baidu.com, 用例中的地址简写即可" text="地址"/>,
      key: 'gateway',
      dataIndex: 'gateway',
      render: text => <a href={text}>{text}</a>,
      ellipsis: true
    },
    {
      title: '操作',
      key: 'operation',
      render: (_, record) =>
        <>
          <a onClick={() => {
            setItem(record)
            setModal(true)
          }}>编辑</a>
          <Divider type="vertical"/>
          <PityPopConfirm text="删除" title="你确定要删除这个地址吗?" onConfirm={async () => {
            await onDelete(record)
          }}/>
        </>

    }
  ]

  const fields = [
    {
      name: 'env',
      label: '环境',
      required: true,
      message: '请选择对应环境',
      type: 'select',
      component: <Select placeholder="请选择对应环境">
        {envList.map(v => <Option key={v.id} value={v.id}>{v.name}</Option>)}
      </Select>,
    },
    {
      name: 'name',
      label: '地址名称',
      required: true,
      message: '请输入地址名称',
      type: 'input',
      placeholder: '请输入地址名称',
    },
    {
      name: 'gateway',
      label: '服务名',
      required: true,
      message: '请输入服务地址',
      type: 'input',
      placeholder: '请输入服务地址',
    }
  ];

  // 删除地址
  const onDelete = async record => {
    const ans = await dispatch({
      type: 'gconfig/deleteAddress',
      payload: {
        id: record.id,
      }
    })
    if (ans) {
      fetchAddress()
    }
  }

  // 新增/修改地址
  const onSubmit = async values => {
    let ans;
    if (item.id) {
      ans = await dispatch({
        type: 'gconfig/updateAddress',
        payload: {
          ...values,
          id: item.id,
        }
      })
    } else {
      ans = await dispatch({
        type: 'gconfig/insertAddress',
        payload: values
      })
    }
    if (ans) {
      setModal(false)
      fetchAddress()
    }

  }


  return (
    <PageContainer breadcrumb={null} title="请求地址管理">
      <Card>
        <FormForModal visible={modal} fields={fields} title='添加地址' left={6} right={18} record={item}
                      onFinish={onSubmit} onCancel={() => setModal(false)}/>
        <Form form={form} {...CONFIG.LAYOUT} onValuesChange={fetchAddress}>
          <Row gutter={12}>
            <Col span={3}>
              <Form.Item>
                <Button type="primary" onClick={() => setModal(true)}><PlusOutlined/>添加地址</Button>
              </Form.Item>
            </Col>
            <Col span={7}>
              <Form.Item label="环境" name="env">
                <Select allowClear showSearch placeholder="选择对应的环境">
                  {envList.map(item => <Option value={item.id} key={item.id}>{item.name}</Option>)}
                </Select>
              </Form.Item>
            </Col>
            <Col span={7}>
              <Form.Item label="名称" name="name">
                <Input placeholder="输入对应的地址名称"/>
              </Form.Item>
            </Col>
            <Col span={7}>
              <Form.Item label="地址" name="gateway">
                <Input placeholder="输入对应的地址"/>
              </Form.Item>
            </Col>
          </Row>
        </Form>
        <Table columns={columns} loading={isLoading} rowKey={record => record.id} dataSource={addressList}/>
      </Card>
    </PageContainer>
  )
}

export default connect(({gconfig, user, loading}) => ({gconfig, user, loading}))(memo(Address));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

当查询表单的数据变化时,我们会自动重新获取查询接口。那么关于具体的使用细节,我们将在下一章来完善,敬请期待~

最新体验地址: https://pity.fun (opens new window)