Skip to content

142.通过har文件生成用例

大家好~我是米洛
我正在从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;

其实具体细节也很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>

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

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

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

最后来看看效果

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

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