测试平台系列(142) 通过har文件生成用例.md

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

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

# 回顾

温故而知新,特别是很久不来跟系列的读者,每次看看回顾都挺不错的,还记得上一节讲了啥。

上一节我们编写了导入har的接口,这一节我们就实现前端部分去配合它。

# 编写前端部分

还记得之前我们编写的用例录制页面吗?我决定把它的table部分抽离出来,因为这块是可以复用的。

并且我给自己定了一个小目标,后续的组件都基本上用tsx开发,虽然很艰难,但是有压力就有动力,何况tsx完美兼容jsx。

  • 提取table部分,新建src/components/TestCase/recorder/RequestInfoList.tsx
import React from "react";
import type {ColumnsType} from "antd/lib/table/Table";
import {Modal, Table, Tag, Tooltip} from "antd";
import SyntaxHighlighter from "react-syntax-highlighter";
import {vs2015} from "react-syntax-highlighter/dist/cjs/styles/hljs";
import {TableRowSelection} from "antd/lib/table/interface";
import NoRecord from "@/components/NotFound/NoRecord";


interface RequestProps {
  index: number;
  url: string;
  request_method: string;
  status_code: number | string;
  response_headers: string;
  request_headers: string;
  body: string;
}


interface RequestInfoProps {
  dataSource: Array<RequestProps>;
  rowKey?: string;
  rowSelection: TableRowSelection<any>;
  loading?: boolean;
  emptyText?: string | '暂无数据';
}

interface TagProps {
  color: string;
  fontColor: string;
}

const tagColor = (method: string): TagProps => {
  switch (method.toUpperCase()) {
    case "GET":
      return {color: 'rgb(235, 249, 244)', fontColor: 'rgb(47, 177, 130)'}
    case "POST":
      return {color: 'rgb(242, 244, 248)', fontColor: 'rgb(5, 112, 175)'}
    case "PUT":
      return {color: 'rgb(255, 247, 230)', fontColor: 'rgb(255, 174, 0)'}
    case "DELETE":
      return {color: 'rgb(253, 244, 246)', fontColor: 'rgb(222, 72, 108)'}
    default:
      return {color: 'rgb(243, 251, 254)', fontColor: 'rgb(166, 187, 210)'}
  }
}

const MethodTag = ({color, text, fontColor}) => {
  return <Tag style={{color: fontColor, borderRadius: 12, padding: '0 12px'}} color={color}>{text}</Tag>
}

const Detail = ({name, record}) => {
  return <a onClick={() => {
    Modal.info({
      title: name,
      width: 700,
      bodyStyle: {padding: -12},
      content: <SyntaxHighlighter language="json" style={vs2015}>{record[name]}</SyntaxHighlighter>
    })
  }}>详细</a>
}

const RequestInfoList: React.FC<RequestInfoProps> = ({dataSource, loading, ...restProps}) => {
  const columns: ColumnsType<RequestProps> = [
    {
      title: '编号',
      key: 'index',
      render: (text, record, index) => `${index + 1}`
    },
    {
      title: '请求地址',
      key: 'url',
      dataIndex: 'url',
      width: '20%',
      render: url => <Tooltip title={url}><a href={url}>{url.slice(0, 48)}</a> </Tooltip>
    },
    {
      title: '请求方式',
      key: 'request_method',
      dataIndex: 'request_method',
      render: md => <MethodTag fontColor={tagColor(md).fontColor} color={tagColor(md).color} text={md}/>
    },
    {
      title: '请求headers',
      key: 'request_headers',
      dataIndex: 'request_headers',
      render: (request_headers, record): React.ReactNode => {
        return <Detail name="request_headers" record={record}/>
      }
    },
    {
      title: '请求参数',
      key: 'body',
      dataIndex: 'body',
      render: (body, record) => {
        if (!body) {
          return '-'
        }
        return <Detail name="body" record={record}/>
      }
    },
    {
      title: '返回headers',
      key: 'response_headers',
      dataIndex: 'response_headers',
      render: (response_headers, record) => {
        if (!response_headers) {
          return '-'
        }
        return <Detail name="response_headers" record={record}/>
      }
    },
    {
      title: 'response',
      key: 'response_content',
      dataIndex: 'response_content',
      render: (response_content, record) => {
        if (!response_content) {
          return '-'
        }
        return <Detail name="response_content" record={record}/>
      }
    },
  ]


  return (
    <Table columns={columns} pagination={false} dataSource={dataSource}
           rowSelection={restProps.rowSelection} rowKey={record => record[restProps.rowKey]}
           loading={loading} locale={{emptyText: <NoRecord desc={restProps.emptyText} height={150}/>}}/>
  )
}

export default RequestInfoList;

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

其实具体细节也很jsx差不多,由于ts我还不是很熟,我暂时把它当做PropType在用(以前早期的时候我们都会定义PropType保证组件的入参)。

这里我定义了RequestInfoProps参数,作为RequestInfoList组件的入参,有一部分可以省略,比如emptytext,也可以给默认值

可以看到,tsx除了一些变量类型的声明,interface的定义,<泛型>的使用,其他和jsx也没有太大区别(安慰你们,也安慰我自己,可以尝试下嘛)

对于我来说,尽量不用any就行了,免得写成了any script.

抽出组件以后呢,我们就需要传入dataSource了,因为这块内容是会由他的父组件决定。

接着我们编写父组件部分,基本上根据用户点击har导入录制数据按钮,就可以决定dataSource了,如果dataSource为空,还得给个友好提示,让用户去录制页面录制去。

我们的这个表单是放到Drawer的,所以我们需要编写一个Drawer的tsx,并把开启/关闭抽屉的权利赋给TestCaseDirectory.jsx组件。

由于前端代码很占篇幅,所以我们抽一部分出来讲解:

    <Drawer title="生成用例" onClose={() => setVisible()} visible={visible} width={960} extra={
      <Button onClick={onGenerateCase} type="primary"><FireOutlined/> 生成用例</Button>
    }>
      <Form form={form} {...CONFIG.SUB_LAYOUT}>
        <Row gutter={8}>
          <Col span={12}>
            <Form.Item label="用例目录" name="directory_id" rules={[{required: true, message: '请选择用例目录'}]}>
              <TreeSelect placeholder="请选择用例目录" treeLine treeData={directory}/>
            </Form.Item>
          </Col>
          <Col span={12}>
            <Form.Item label="用例名称" name="name" rules={[{required: true, message: '请输入用例名称'}]}>
              <Input placeholder="请输入用例名称"/>
            </Form.Item>
          </Col>
        </Row>
      </Form>
      {
        record.length === 0 ?
          <Empty image={NoRecord} imageStyle={{height: 220}} description="当前没有任何请求数据,你可以选择【录制】后的数据,也可以导入har文件提取接口👏">
            <Space>
              <Button onClick={onLoadRecords}><CameraOutlined/> 录制请求</Button>
              <Upload showUploadList={false} customRequest={onUpload} fileList={[]}>
                <Button type="primary">
                  <ImportOutlined/>
                  导入Har
                </Button>
              </Upload>
            </Space>
          </Empty> :
          <RequestInfoList dataSource={record} rowSelection={rowSelection} rowKey="index"
                           loading={loading.effects['recorder/generateCase']}/>
      }
    </Drawer>
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

这个类似html的页面,是我们的视图层。我们的布局是这样, 对着代码看界面:

上面2个字段属于表单也就是Form,下面的字段属于RequestInfo组件或者Empty(随着是否有数据切换),接着就是右上角的生成按钮。

后续对着编写事件即可,其实前端也不算复杂,简单的还是很好写的,就是进阶感觉很难。

# 最后来看看效果

别忘记生成后关闭对话框,并且重新获取目录下的case哦~

这样,一份粗糙的用例生成就完事了,其实还有用例录制页面没有完成,那个就先搁着吧,需要研究一下mitmproxy中的一个参数,暂且卖个关子。