目录
2.2.2 增加对应后端接口的信息:config/proxy.ts
2.2.3 页面展示文件 index.tsx: src/pages/CreateCase/index.tsx
2.2.4 运行结果展示文件 ExecResultItem.tsx: src/pages/CreateCase/components/ExecResultItem.tsx
2.2.5 页面接口文件 services.ts: src/pages/CreateCase/services.ts
2.2.6 页面接口文件 data.d.ts: src/pages/CreateCase/data.d.ts
2.3.2 对应后端接口的信息:config/proxy.ts
2.3.3 页面展示文件 index.tsx: src/pages/CaseList/index.tsx
2.3.4 页面接口文件 services.ts: src/pages/CaseList/services.ts
2.3.5 页面数据类型定义文件 data.d.ts: src/pages/CaseList/data.d.ts
2.4.1 路由信息routes.ts:config/routes.ts
2.4.2 对应后端接口的信息:config/proxy.ts
2.4.3 页面展示文件 index.tsx: src/pages/AutoList/index.tsx
2.4.4 展示用例结果列表的组件 ExecHistory.tsx: src/pages/AutoList/components
2.4.5 展示结果详情列表的组件 ExecResult.tsx: src/pages/AutoList/components
2.4.6 页面接口文件 services.ts: src/pages/AutoList/
2.4.7 页面数据类型定义文件 data.d.ts:src/pages/AutoList/
3.1 工程启动类(ApplicationServer.java)
3.2 Entity实体类(domain) :一个实体类对应一个表格
3.2.1 用例Case.java: /entity/Case/Case.java
3.2.2 自动化用例AutoCase.java: /entity/auto/AutoCase.java
3.2.3 自动化结果概要AutoResult.java: /entity/auto/AutoResult.java
3.2.4 自动化结果详情AutoResultDetail.java: /entity/auto/AutoResultDetail.java
3.3.2 目录/dto下的:CaseDto, HttpDto, PagingQueryCaseDto, /auto/AutoCaseDto, /auto/PagingQueryAutoCaseDto
3.3.3 目录/http下的:HttpHeader, HttpRequest, HttpResponse
3.3.4 目录/response下的:RespBean, PageRespBean
3.4 数据接口访问层(Dao),也就是Repository
3.5.2 自动化用例的业务程序 AutoCaseService
3.6.2 自动化用例的控制程序 AutoCaseController
3.8 异常处理: handler/DefaultExceptionHandler
3.10 配置文件(./yaml/.properties/.json等)置于config文件夹下
3.11 工具类(utils)置于com.springboot.utils
4.5 在http.ts中隐藏删除等接口,依次测试删除等功能
4.6 这里梳理一下前端如何识别到后端,并调用后端的命令来实现效果的?
1. Promise
(1)为什么要讨论promise?
跟获取request的结果有关系。
如果在request里面加await拿到的promise, 跟去掉await后定义一个testPromise函数new 一个promise,然后resolve获取promise信息,是一样的。
现在用的比较多的是 sync和 await,所以promise了解即可。
(2)export const testPromise = () => {return request('/api/todos', { })} :
这个操作,request拿到的是promise, 这里展开讲了promise是什么?然后讲通过async和await的方式,来简化,获取promise里具体值。前端代码中request调用上述操作接口,需要拿到的是promise里的具体的data, success, total…
(3)先记下常用套路:前端代码中request调用语句中:接 async, 调接口语句前加 await。
(4)Promise的概念:
首先,promise是js的抽象异步处理对象实现异步编程的方案。简单的说,就是解决传统js运行单线程的诟病以及异步操作的多层级嵌套带来的麻烦。
从用途上来说:
—promise主要用于异步计算。
—可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。
—可以在对象之间传递和操作promise,帮助我们处理队列。
从语法上说:
—Promise 是一个对象,从它可以获取异步操作的消息。
—Promise 提供统一的API,各种异步操作都可以用同样的方法进行处理。
(5)Promise的基本用法?
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
2. 接口自动化平台前端开发
2.1 前端环境搭建
VSCode工具
antd pro组件库
2.2 新建用例页 CreateCase
2.2.1 增加路由信息:config/routes.ts
完整代码略
2.2.2 增加对应后端接口的信息:config/proxy.ts
完整代码略
2.2.3 页面展示文件 index.tsx: src/pages/CreateCase/index.tsx
完整代码略
2.2.4 运行结果展示文件 ExecResultItem.tsx: src/pages/CreateCase/components/ExecResultItem.tsx
作为组件的形式,被index.tsx调用
2.2.5 页面接口文件 services.ts: src/pages/CreateCase/services.ts
完整代码略
2.2.6 页面接口文件 data.d.ts: src/pages/CreateCase/data.d.ts
完整代码略
2.2.7 一些备注事项
(1) 一些数据结构的定义字段除了可以放在data.d.ts中之外,也可以直接放在index.tsx中,放在组件定义(const CreateCase: FC = ( )=>{ })之前。
比如根据新建用例时,根据页面要展示的内容,来在该index.ts中直接定义这样的字段interface CaseInfo{ },供这个页面使用。这些字段的初始化(置空)赋值也放在该位置。
(2)组件中除了定义一些state变量 + const columns: PreColumnType<> + return 渲染部分之外,还有什么?
------各种方法定义:比如const onFinish = async () =>{}, onValueChange, handleExec等。
------useEffect(()=>{}, [ ]), 功能类似react的生命周期中的didMount. 在didMount阶段和willUnmount阶段,都会执行。
(3)详细说说useEffect()
这是在组件主渲染之前先做的一个动作,比如通常里在里面实现某些变量的初始化。
备注:const { id } = useParams<{ id?: string }>(); //编辑用例, useParams是框架给的。useParams返回URL参数的键/值对的对象。
解析:
1)第一个参数:
实现/定义的是一个函数(无参),箭头函数。函数体{ }中,用一个括号包起所有函数语句,后面再接一个空括号,就相当于对这个函数进行了调用。如果没有后面这个括号,就相当于只是定义了函数而已。不符合useEffect第一个参数的要求。
第二个参数是限制了在什么情况下(以哪些内容为监控标准),会执行第一个参数(或者说useEffect才会有效)。如果为空,则只在第一次加载页面或者刷新时,才会执行第一个参数。
2)这里的初始化功能用在了两种情况; 新建用例和编辑用例。因为这两种情况都是在同一个页面(pages/CreateCase/)中进行。区别就是初始化值不同;
新建用例时,初始化用置空字段,即没有传递id,也就是上面的代码中不走id路线,直接setInitData(emptyInit)。
编辑用例时,会通过useParams获取id值,根据id值,传参调用getCase接口方法(定义在service.ts中),获取该id的case信息,用这个信息来初始化setInitData(res?.data); return true;
3)新建用例时,为了先让页面架构使用上面的useEffect进行初始化,可以使用以下句式:
return ( initData ? <Form></Form> : <Pageloading /> )
解析:
意思就是先判断初始化字段是否为undefined?
如果undefined的话,先<Pageloading/>加载页面,同时框架利用useEffect来初始化initData.
初始化后再判断一次,此时不是undefined了,即刻进入<Form>表单。
注意还要在Form标签的属性里使用initialValues这个属性,如下:
<Form initialValues={initData} //初始化 >
4)现在回到新建用例页面的主体部分return().
因为新建的用例要提交到服务器,所以要把所有的内容放到表单<Form></Form>里。
<Form>表单里再放<PageContainer>和<FooterToolbar>。
<PageContainer>中还是用<Card>美化。
<Card></Card>里面就是多个<Form.Item></Form.Item>。
每个Form.Item表示一个表单项。如果想让多个Form.Item在同一行,用<Input.Group compact></Input.Group>括起来。
5) <Form>标签中的常用属性:
// hideRequiredMark, 还可考虑加这个
这里重点说:onFinish={onFinish} //submit时调用.
即最下面的<FooterToolbar>里有一个按钮,按钮点击事件时使用的是form?.submit()方法提交,提交表单并且数据验证成功时,就调用了这个onFinish方法。
执行onFinish的回调函数时,先判断现在要进行的是新增用例还是编辑用例?怎么判断?通过id。
如果上面有通过useParams获取到id, 那说明要执行编辑用例的操作,否则就进行新增用例操作。这两个操作都在service.ts里定义。
该onFinish操作结束后,跳转到用例列表页。
onValuesChange={onValueChange} //只要是Form里的值发生了变化,它都能监听到
if (!isPost) { //如果不是post,即从post切换到其他方法,则清空区域。再回到post值也没了
form.setFieldsValue({
body: '',
caseName: 'xxx', })
}
6)<Card>里有一个执行按钮,所以在Card的属性里加一个extra属性,然后加按钮,按钮实现“执行”功能。点击按钮,调用回调函数。
handleExec() “执行”方法的定义:
这里直接调用form表单里的form.getFieldsValue()方法获取当前表单页面的信息(form来源于: const [form] = Form.useForm(); //antd自己的 + 在<Form>属性里设置的form={form})。
然后通过传参,调用接口方法execCase()(service.ts里定义)来实现“执行”操作。
最后将返回的执行结果保存到state变量execResult里:
7)点击执行后,执行结果如何展示?通常用一个普通的模态框 <Modal></Modal>来展示。
------这个模态框放在哪儿?模态框可以放<PageContainer>内,跟<Card>平齐。
------如何控制模态框出现与否?根据是否有执行结果来控制。
上一步执行步骤进行后,将结果赋值给state变量execResult(是一个autoResultItem),这里就根据execResult来控制模态框的visible。
------模态框页面上应该显示哪些内容,如何显示?
这里可以单独定义一个组件<ExecResultItem data={execResult} />来实现这部分。
将执行结果传进组件,组件用return(<Card><Descriptions>……</Decriptions></Card>)来展示。
8)<Form.Item></Form.Item>之间,可以包含以下这些组件:
<Input></Input>,
<Select><Select.Option></Select.Optino></Select>
<Input.TextArea />
<Form.List></Form.List>等
这里重点说<Form.List></Form.List>,这里动态的增加行,每一行是键值对的形式。
具体代码比较长,直接在antd 组件库里搜相应的效果的代码,拷贝后,修改部分的参数值,达到自己要的效果即可:
<Form.List name="headers">
{(fields, { add, remove }) => (<>...................</> )}
9) 后面针对新建性能测试用例时,需要增加“正则表达式”和“响应断言”的内容,所以又补充了一个<Card>, 里面放两个可切换的<Tabs>。详情在下一篇性能用例文章中阐述。
10)<FooterToolbar>组件,用来提交新建用例的表单内容。
2.3 用例列表页 CaseList
2.3.1 路由信息config/routes.ts
2.3.2 对应后端接口的信息:config/proxy.ts
同 2.2.2
2.3.3 页面展示文件 index.tsx: src/pages/CaseList/index.tsx
完整代码略
2.3.4 页面接口文件 services.ts: src/pages/CaseList/services.ts
2.3.5 页面数据类型定义文件 data.d.ts: src/pages/CaseList/data.d.ts
2.3.6 一些备注事项:
(1)一个列表页面,(函数)组件的大框架包括3个部分:
1)const 定义一些参数和改变参数的方法(useState)。
2)const columns: PreColumnType<> = [ { }, { }…] 定义列表各列。 每一个{ }代表一列:可以用dataIndex直接调用后台接口的某些值(直接展示,无跳转无链接等)。 还可以在某些列实现一些动态效果,比如:用例名的抽屉显示,删除操作,编辑操作,执行操作等。此时dataIndex可有可无,用render:(_, item) => { return( ) }来实现这些操作。
3)return ( < > < />)返回要渲染到页面的内容和动作。
(2)展开说第3)点:return()
return后接(),()里必须只有一个根标签。
通常页面用<PageContainer>作为根标签,这个标签自带一些标题格式和内容。
<PageContainer>标签对之间,放<ProTable>单标签,<Drawer>双标签等。
<PageContainer>标签属性里,可以放一般属性,也可以放类似 footer={ } 作为页面底部的内容/操作/按钮(弹窗控制)等。
(3)<ProTable />单标签里,放了页面的主要内容。
比如:
1)列表项的引用 columns={columns};
2)后端接口的调用 request={async (params) => await getCaseList({ ...params })};
3)工具栏(含“新建用例按钮”等)toolBarRender={ () => [... ] };
4) 列表中每一项前面的选择框 rowSelection={ { onChange: (_, selectedRows) => setSelectedRows(selectedRows) } };
5)其他一些属性等,比如 rowKey(注意:这个不能少,没有这个,在点击选择某个列表项的时候,会出现全选的效果)。
6)比如 actionRef={actionRef} 来标识这个ProTable。
(4)<Drawer>双标签,抽屉效果。
1)通过在属性里设置一些值和回调函数,跟const 定义的一些state参数变化而实现 “抽屉是否展示,在点击什么内容时展示,展示时应该传入哪些值” 等效果。
2)常用属性: title, placement, width, closable, onClose={()=>{setRow(undefined)}}, visible={!!row}。 (row是CaseItem)
3)<Drawer>双标签对之间,通常通过<Descriptions></Descriptions>标签,来实现抽屉页面内容的逐行展示。<Descriptions>属性中比较重要的是column={1},这么设置最终的效果就是有两行。
(5)通常会将一些数据结构的定义(包含哪些字段,以及每个字段的数据类型)放在data.d.ts中。然后用CaseItem作为页面上很多项的泛型 <CaseItem>, 限制某些项的数据结构类型,具体内容如图。
(6)通常会将所有调用接口的方法定义放在一个统一的文件service.tsx里。
如下,其中 `/api/case/list`对应于后端所暴露出的后端接口
注意:这里的接口,很多时候可以用反单引号,这个反单引号的作用就是:里面可以直接取参数值,比如${caseId}, 适用于接口路径中带动态参数的情况的实现.
然后,参数看情况来传递,比如:
(7)如何实现页面跳转?用history.push()。
注意这里的history来自“umi”。
onClick={() => { history.push(`/editCase/${ item.id}`); }} //这里反引号,里面可以直接用变量。
(8) 一些操作后需要弹出一个新的框,在新框里展示内容或者实现操作,比如模态框。
常见的一种确认模态框,用Modal.Confirm({......}).
比如删除操作,就需要弹出一个确认框来确认是否要删除。
onClick={async () => { Modal.confirm({ }) } }。
属性里最常用的是: title, content, okText, cancelText, onOk: async () => { }.
最后这个回调函数里,实现了删除方法的回调(删除方法在service.ts中定义并暴露)。
(9) 模态框不仅限于确认模态框,常用还有一种针对“需要有内容提交的”表单模态框,比如<ModalForm>表单模态框,双标签:<ModalForm></ModalForm>:
1)可用泛型来限制提交的结构类型,比如<ModalForm<{autoCaseName: string}>。
2)常用的属性:key, title,trigger={ 里面可定义一些按钮标签,用来实现触发动作},onFinish ={async ( ) => { 里面实现一些函数的回调,实现点击按钮后的一些函数/方法/动作 }}。 见下方图。
3)表单模态框里,双标签中间,通常用<ProForm.Group><ProFormText 各个属性/></ProForm.Group>,来在表单模态框中显示内容。
(10) 调用后端接口的语句:request={async (params) => await getCaseList({ ...params })}。
1)getCaseList()在service.ts中定义。
2)如果await getCaseList({ ...params })加花括号,要记得同时加return, 否则报错。
(11) 一个比较严谨的做法:调用某个变量之前,先判断它是否为真,为真的话再继续调用这个变量的相关操作。 {row?.id && (……)}
(12) service.ts中定义的接口方法中常见的参数格式:
async (params?: any) => { }
async (caseId?: number) => { }
async (params: {autoCaseName: string; caseIds: number[]}) => { }
2.4 自动化用例集列表页 AutoList
2.4.1 路由信息routes.ts:config/routes.ts
2.4.2 对应后端接口的信息:config/proxy.ts
同 2.3.2, 此处截图略
2.4.3 页面展示文件 index.tsx: src/pages/AutoList/index.tsx
完整代码略
2.4.4 展示用例结果列表的组件 ExecHistory.tsx: src/pages/AutoList/components
完整代码略
2.4.5 展示结果详情列表的组件 ExecResult.tsx: src/pages/AutoList/components
完整代码略
2.4.6 页面接口文件 services.ts: src/pages/AutoList/
2.4.7 页面数据类型定义文件 data.d.ts:src/pages/AutoList/
2.4.8 一些备注事项
(1)还是三部分:state变量,columns:ProColumnType<>,return()里用<PageContainer>双标签括起:<ProTable>单标签 + <Drawer>双标签。前者用来展示页面的所有内容。后者用来展示每个用例集的详细内容,抽屉效果。
(2) const columns: ProColumnType<AutoListItem>[] = [ ]
1)AutoListItem数据结构先在data.d.ts中定义好,根据页面要展示的项来定义。
2)然后逐个定义columns, 类似用例列表页。这里不重复。
3)其中, autoCaseName定义成<a>标签的形式,点击时调用getCasesByAutoCaseId接口方法,实现:查询单个自动化用例集id关联的case。传递的是所点击的自动化用例集项的id。返回的是接口`/api/auto/${autoCaseId}/cases`的内容。注意反引号的使用。
定义一个state参数,用来存放点击用例集项后返回的该用例集所包含的多个关联case信息,如下。下方<PageContainer>里的<Drawer>将会个根据这个autoCases来决定:是否显示抽屉?以及抽屉里具体展示哪些内容?
4)column里的“操作”包含两个动作:删除和执行。分别都用<a>标签来表示。
删除的<a>标签中,用onClick执行一个回调函数,回调函数即使用Modal.Confirm确认模态框组件,模态框的onOK:()=>{}里调用删除用例集的接口方法deleteAutoCase(item.id)。传的是用例集的id, 返回的是接口`/api/auto/${autoCaseId}`的数据。
执行的<a>标签中,onClick的回调函数,调用接口方法execAutoCase(item?.id)。返回的是这个接口:
return request(`/api/auto/exec/${autoCaseId}`, {
method: 'post', })
//用例集的执行不需要重新加载表格数据,所以不需要reload。
(3)根据上面的autoCaseName,这里一起说一下<PageContainer>里的<Drawer>。
这里<Drawer>结构整体和之前类似,比较特殊的一点是:这里获取到的详细信息是包含多个cases, 而前面的只是一个case。 所以这就需要考虑如何在抽屉里展示多个cases的详细内容。用map()方法遍历。
(4)这里重点说列表的折叠效果expandable。
<ProTable>单标签内容如下:
在ProTable的API里找能实现折叠展开效果的expandable,拷贝引用。
因为显示展示内容的步骤比较多,这里单独定义一个组件<ExecHistory />来展示所点击的某个用例集项所包含的cases信息。
(5) <ExecHistory>组件:
1)useEffect初始化
//在组件初始化的时候,先请求执行历史列表数据,然后再渲染在组件上
其中,//查询单个自动化用例集的执行历史列表接口
2)要展示用例集下的多个cases, 因为外部已经有ProTable了,这里就不重复用Table了,用一般的List就行。
<List> 套<List.Item>套 <Row>套<Col>。<List.Item>表示一行。
<List>单标签,属性里有dataSource={data??[]}。 data就是上面useEffect时初始化返回的列表数据。
注意上面的两个部分。
一个是如何定义列表的每一行。
一个是列表每一行的最后一项是执行详情,设为<a>标签,onClick后触发一个state参数的变化,进而触发出一个模态框。这个模态框只是用来展示数据,所以用常规的<Modal>即可。在<Modal>双标签里再调用一个自定义的组件<ExecResult id={crtResultId} />。 用来展示该case的详情(请求信息,响应结果…)
<Modal>也定义在return里,跟<List>平齐。
3)上面的<ExecResult id={crtResultId} />组件,同样的套路:
3. 接口自动化平台后端开发
3.1 工程启动类
(1)项目启动时,其当前层及子目录中所有controller会被编译。
(2)可通过直接运行该类来启动Spring Boot应用。
(3)带有注解 @SpringBootApplication
(这里另一个注解 @EnableJpaAuditing跟获取时间有关系)
3.2 Entity实体类(domain) :一个实体类对应一个表格
小结:
(1)一般创建后,项目启动后,如果没有表格,会在数据库中自动生成一个表格。
(2)实体中的各个属性,对应了表格中的每一列。定义属性,相当于定义表字段。
(3)注意各个注解的使用:
1)这里用到表格,所以需要在@Table(name = "test_case")位置会要求加datasource, 也就是在这个位置要引入数据库表格信息,如果没有,则在IDEA右侧database那里,新增一个自己MYSQL数据库相关的配置信息。如果驱动没下载,则下载。
2)加datasource后,表名位置可能还有红线,因为可能是表还没创建的原因,可忽略红线。在SQLyog连接数据库之后,看看红线还有没有。此时已经定义好了实体类,可以通过重启SpringBoot后,可以自动生成表(刷新或重连后看到)。
3)然后重启IDEA。
3.2.1 用例Case.java: /entity/Case/Case.java
3.2.2 自动化用例AutoCase.java: /entity/auto/AutoCase.java
3.2.3 自动化结果概要AutoResult.java: /entity/auto/AutoResult.java
3.2.4 自动化结果详情AutoResultDetail.java: /entity/auto/AutoResultDetail.java
3.3 Bean实体类
小结:
(1)定义多个bean类, 每个bean类有多个private属性,通过getter, setter的方式获取或赋值。对应不同的前端页面(跟前端页面的展示有联结)。
(2)定义这些实体类,Swagger自动匹配上Bean相关的一些内容,在页面上将Bean的整个结构以JSON的结构/方式标出来。好处是,更改每个Bean实体类中的信息,重启SpringBoot后,Swagger中(接口文档)就会自动更新了。如果不用Swagger,就既要改代码又要改接口文档。
3.3.1 目录/common下的:Paging.java
(1)Paging.java
在分页查询时,需要用到的信息:每页展示的用例数,当前是第几页
3.3.2 目录/dto下的:CaseDto, HttpDto, PagingQueryCaseDto, /auto/AutoCaseDto, /auto/PagingQueryAutoCaseDto
(1)CaseDto
1)对实体类Case的一些展示,因为Case直接对应数据库表,很多时候,不适合将数据库表中的所有数据都展示在前端,因此借助CaseDto,对Case做一层封装(或者说转换),想在展示Case的什么属性,就在dto中定义该属性就行,不想展示的就不定义。
2)两个构造方法,参数不同,后者多了:正则提取器实体类,响应断言实体类
(2)HttpDto
整合了两个实体类 HttpRequest和HttpResponse, 为后续统一展示做准备。
(3)PagingQueryCaseDto
补充用例分页查询要展示的项:用例名称
前面定义的Paging中只包含了current和pageSize
(4)/auto/AutoCaseDto
(5)/auto/PagingQueryAutoCaseDto
3.3.3 目录/http下的:HttpHeader, HttpRequest, HttpResponse
(1)HttpHeader
(2)HttpRequest
(3)HttpResponse
3.3.4 目录/response下的:RespBean, PageRespBean
(1)RespBean
1)定义该实体类,用于定义返回信息:
状态,
提示信息,
返回的数据:返回的数据可能是不同类型的,所以这个返回数据用了泛型T。注意,定义实体类的时候,就要带<T>。
(2)PageRespBean
分页查询时,返回的内容跟其他操作略有不同,所以定义一个实体类,继承上面的RespBean, 补充一些属性,并重写RespBean中的方法。
3.4 数据接口访问层(Dao),也就是Repository
3.4.1 用例的Repo:CaseRepo
(1)CaseRepo
3.4.2 自动化用例的Repo:3个
repository是数据库表和实体类entity之间的衔接作用。
(1)/auto/AutoCaseRepo
(2)/auto/AutoResultRepo
(3)/auto/AutoResultDetailRepo
3.5 数据服务层(Service)
1)功能跟controller联系紧密。相当于在repository和controller之间的衔接作用。
2)repository是数据库表和实体类entity之间的衔接作用。
3)@Service 注解的作用是:让SpringBoot托管这个service。
3.5.1 用例的业务程序 CaseService
3.5.2 自动化用例的业务程序 AutoCaseService
3.6 前端控制器(Controller)
小结:
(1)置于com.springboot.controller:
用于写入参,定义不同的页面的接口。
通过这不同的接口,可以访问到不同的页面。比如post 页面路径,get页面路径...
(2)注解介绍
@RestController 标签等价于@Controller+@ResponseBody。
@Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。当然也有语义化的作用,即代表该类是充当Controller的作用。
@ResponseBody 它的作用就是指该类中所有的API接口返回的数据,甭管你对应的方法返回Map或是其他Obejct, 它会以Json字符串的形式返回给客户端。
@Autowired 注入注解,是按类型自动装配的,不支持id匹配
3.6.1 用例的控制程序 CaseController
3.6.2 自动化用例的控制程序 AutoCaseController
3.7 配置信息类(config)
小结:
Swagger类,置于com.springboot.config
Config/Swagger: 帮助我们接口定义文档。每当更新一版代码,Swagger自动快速生成接口文档。
里面的内容不用背,网上抄的。
config/SwaggerConfig 注意:
.apis(RequestHandlerSelectors.basePackage("com.road.testdev.autotest.controller"))
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
// @Value("true")
private boolean enable = true;
@Bean
public Docket createDocke() {
return new Docket(DocumentationType.SWAGGER_2)
//进入swagger-ui的信息
.apiInfo(apiInfo())
.select()
//暴露所有controller类的所在的包路径
.apis(RequestHandlerSelectors.basePackage("com.road.testdev.autotest.controller"))
.paths(PathSelectors.any())
.build()
.enable(enable);
}
//进入swagger-ui的信息
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//该项目的名字
.title("自动化测试平台Api说明文档0903")
//该项目的描述
.description("自动化测试平台接口文档0903")
.version("1.0")
.build();
}
}
3.8 异常处理: handler/DefaultExceptionHandler
小结:
捕获异常,这里没有很复杂或者需要改动的,就直接套用这个模板就行。
测试时,可以在casecontroller里输一些异常语句,在Swagger中执行casecontroller中相应的语句对应的页面,它就会自动捕获Exception及其子类的异常。然后在页面的响应结果的部分,看到异常结果。
小结:
(1)这部分底层用了Springboot的切面原理,AOP. 即切到这个项目下各个模块,监控哪个模块有异常。
(2)@ControllerAdvice:
是Spring3.2提供的新注解,它是一个Controller增强器, 可对controller中被 @RequestMapping注解的方法加一些逻辑处理。最常用的就是异常处理。
使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解。
在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法...,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。
(3)@ExceptionHandler :
注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。
import com.road.testdev.autotest.bean.response.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@Slf4j
public class DefaultExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public RespBean exceptionHandler(Exception e){
e.printStackTrace();
log.error("Default Handler: {}", e.getStackTrace());
return RespBean.failure(e.toString(), "Default Handler");
}
}
3.9 枚举类enums/RespCode
3.10 配置文件(./yaml/.properties/.json等)置于config文件夹下
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://xx.xx.xx.xx:3306/test_db1?charset=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=**********
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#server.port=8089
3.11 工具类(utils)置于com.springboot.utils
代码比较长,放在文末
3.12 小结
新增用例: 从Swagger页面的POST接口接收数据,以CaseDto的形式(页面要求输入啥就输入啥)传给controller, 然后controller调用service (传给service的是CaseDto,尽量让service做业务,controller少做),保存数据到数据库表格。然后回到controller里,将Case再转为CaseDto,返回给页面的结果响应。
推荐顺序: dto, repository, service, controller
(一般先实体类,然后repository, service, 最后controller)
4. 前后端联调
写在前面:
下方汇总的一些问题,是写程序过程中遇到的问题,因为程序在不断优化,所以有的问题跟最终版的程序有出入,比如过程中用模拟接口mock/http.tsx等,所以,这些问题可以忽略。仅供过程记录。
4.1 设置代理 config/proxy.ts
延伸知识点:同源策略,做进一步了解。
Antd中设一个代理,相当于我在页面上请求的还是本地的域名和端口,由代理转发,转发到后端的8080端口,pathrewrite(不用动,默认即可)等…
在proxy.ts中的 target配置成自己的localhost和端口,就看你在后端配置文件中设为多少就写多少。
4.2 启动前端工程 antd pro
yarn start
4.3 启动后端工程 spring boot
运行 AutotestApplication
4.4 转换接口数据为后端数据
此时前端页面上还是显示旧的信息,因为此时前端调用的还是之前用来测试的mock/http.ts接口。
在http.ts中将暴露的这个'GET /api/case/list'接口注释掉(后续其他功能也类似,隐藏旧的端口)
再刷新前端,此时前端页面就能顺利调用后端所连接的数据库表格的信息。
至此,页面上展示用例的功能可以了。
4.5 在http.ts中隐藏删除等接口,依次测试删除等功能
前面删除功能没有真正实现,是因为没有重新加载,这里需要修改CaseList 中的index.tsx,在ProTable中加一个属性actionRef={actionRef} (事先定义:const actionRef = useRef<ActionType>())。在调用删除函数之后,调用actionRef.current.reload()重新加载,实现删除功能。
操作中,删除操作还存在一些问题,有时候确认删除后页面上是存在该项,再删一次,或者再删另外一项之后,可能这一项或者这两项才会消失。。。待研究。
后端中确实实现了删除(逻辑删,isdelete为1)
4.6 这里梳理一下前端如何识别到后端,并调用后端的命令来实现效果的?
答:(1)首先是在前端的config/proxy.ts中设置了后端的路径和端口,当操作前端时,默认的就去这个路径和端口下寻找具体的子路径和命令:
(2)在前端页面上点击删除按钮,调用了前端代码中CaseList/index.tsx中的删除相关的模块,模块里调用了service.ts中定义暴露的删除方法:
export const deleteCase = async (caseId: number) => {
return request(`/api/case/${caseId}`, {
method: 'delete',
})
}
其中这个路径,就对应了后端中的删除方法的路径,进而调用了后端的删除方法,实现了对数据库的删除:
@Api(tags = "Case管理相关接口")
@RestController
@RequestMapping("/api/case")
public class CaseController {
@Autowired
CaseService caseService;
…………………………………….
@ApiOperation("删除用例")
@DeleteMapping("/{id}")
public RespBean delCase(@PathVariable("id") Integer id){
Case dCase = caseService.deleteCase(id);
return RespBean.success(new CaseDto(dCase));
}
4.7 新增用例,提交时出现错误
因为:前端定义的用例信息中headers是一个数组,但是目前后端CaseDto中所定义的headers是一个String, 所以这里要改后端中的headers为数组。
Private List<Httprequest.Headers> headers (Httprequest.Headers是一个键值对)
如何操作?
答:
在跟页面直接对接的CaseDto中做修改。
CaseDto接收Case(数据库)来的数据, Case中headers依然保持定义为String.
CaseDto中的headers定义为数组List<Header>,其中Header也单独定义为一个实体,存放键值对。
因此,CaseDto和Case相互转换时,用到了GSon类。
GSon gson = new GSon();
Gson.fromJson: 从JSon List<> (参数相对复杂,记住就行,不用深究)
Gson.toJson: 从List<> JSon
问题:
1. 按照上面的方法,CaseDto转为Case时,已经toJson了,但是Case对应的表格中,headers栏好像还是数组形式,如何解决?
2. 测试过程有一些报错:Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 1 path $
可能是因为有一些历史数据,headers传的是String.后来定义这个为数组了。
处理方式:要么把历史数据清掉,以后都传数组。同时可以try catch Gson那部分的内容,处理异常。
同时,创建handler/DefaultExceptionHandler处理异常。
4.8 查询用例
4.9 编辑用例
存在问题:比如要修改一个POST的用例(带body内容),跳转到修改页面的时候,没有body的内容。如何处理?
4.10 执行用例
1)用到第三方的库,比如httpclient (没有什么套路,直接用别人的)
先添加httpclient的jar包。
建一个util/httpclient : request请求过去后,这个工具类会去调用后端。
补充:深入了解一下httpclient工具类。
(代码中的response有的是真实的response,Headers, apache相关自带的…注意跟自己写的区分)
2)在controller中增加“执行用例的接口”
先看看前端代码中执行用例的部分,调用了下方接口服务,POST方法,传参
export const execCase = async (params: CaseItem) => {
return request('/api/case/exec', {
method: 'post',
data: {
...params
},
})
}
以上,涉及请求和响应的一些内容(属性),所以这里针对Httprequest和HttpResponse实体中的属性,再去包一层dto.(因为最基本的request和response中的所有属性,执行时不一定都用得到),所以,增加了bean/dto/HttpDto。
4.11 几个新的注解
(1)@Data: Lombok 注解, 是以下注解的集合:
@ToString 作用于类,覆盖默认的toString()方法,可以通过of 属性限定显示某些字段,通过exclude 属性排除某些字段。
@EqualsAndHashCode 作用于类,覆盖默认的equals 和hashCode
@Getter 作用类上,生成所有成员变量的getter 方法;作用于成员变量上,生成该成员变量的getter 方法。可以设定访问权限及是否懒加载等。
@Setter 作用类上,生成所有成员变量的setter 方法;作用于成员变量上,生成该成员变量
的setter 方法。可以设定访问权限及是否懒加载等。
@RequiredArgsConstructor 生成包含final 和@NonNull 注解的成员变量的构造器
(2)@NoArgsConstructor: Lombok 注解,用于生成无参构造器
(3)@AllArgsConstructor: Lombok 注解,用于生成全参构造器
(4)@ApiModel: Swagger 注解,表示对类进行说明,用于参数用实体类接收
(5)@ApiModelProperty: Swagger 注解,用于方法、字段,表示对model 属性的说明或者数据操作更改
补充:工具程序HttpClientUtil.java的代码
package com.road.testdev.autotest.utils;
import com.google.gson.internal.LinkedTreeMap;
import com.road.testdev.autotest.bean.http.HttpHeader;
import com.road.testdev.autotest.bean.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpClientUtil {
private static final String DEFAULT_ENCODING = "UTF-8";
public static HttpPost getHttpPost(String url) {
// 创建post方式请求对象
HttpPost httpPost = new HttpPost(url);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000).setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
httpPost.setConfig(requestConfig);
return httpPost;
}
public static HttpResponse sendPostDataByMap(String url, Map<String, String> map, String encoding) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = getHttpPost(url);
// 装填参数
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
if (map != null) {
for (Map.Entry<String, String> entry : map.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
// 设置参数到请求对象中
try {
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, encoding));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 设置header信息
// 指定报文头【Content-type】、【User-Agent】
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
return getResult(response, encoding);
}
public static HttpResponse sendPostDataByJson(String url, String json, String encoding) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = getHttpPost(url);
// 设置参数到请求对象中
StringEntity stringEntity = new StringEntity(json, ContentType.APPLICATION_JSON);
stringEntity.setContentEncoding(encoding);
httpPost.setEntity(stringEntity);
CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
return getResult(response, encoding);
}
public static HttpResponse sendPostDataByRaw(String url, String json, List<HttpHeader> httpHeaders, String encoding) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = getHttpPost(url);
// 设置参数到请求对象中
StringEntity stringEntity = new StringEntity(json, ContentType.TEXT_PLAIN);
stringEntity.setContentEncoding(encoding);
httpPost.setEntity(stringEntity);
for (HttpHeader httpHeader : httpHeaders) {
// LinkedTreeMap header = (LinkedTreeMap) element;
httpPost.setHeader(String.valueOf(httpHeader.getKey()), String.valueOf(httpHeader.getValue()));
}
CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
return getResult(response, encoding);
}
public static HttpResponse sendPostDataByXml(String url, String xml, String encoding) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = getHttpPost(url);
httpPost.addHeader("Content-Type", "text/xml;charset=UTF-8");
// 设置参数到请求对象中 text/xml和application/xml的区别
StringEntity stringEntity = new StringEntity(xml, encoding);
stringEntity.setContentEncoding(encoding);
httpPost.setEntity(stringEntity);
CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
return getResult(response, encoding);
}
//为了用例的执行功能,做了改动。(String url, List<HttpHeader> headers, String encoding),但依旧没有解决问题
public static HttpResponse sendGetData(String url, List<HttpHeader> headers, String encoding) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建get方式请求对象
HttpGet httpGet = new HttpGet(url);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000).setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
httpGet.setConfig(requestConfig);
// Content-type设置为application/x-www-form-urlencoded 或者不设置也是可以的(默认为application/x-www-form-urlencoded)
httpGet.addHeader("Content-type", "application/json");
// 通过请求对象获取响应对象
//为了用例的执行功能,做了改动。改动前HttpHeader element : headers
//for (Object element : headers) {
for (HttpHeader header : headers) {
// LinkedTreeMap header = (LinkedTreeMap) element;
//为了用例的执行功能,做了改动。但依然失败
//HttpHeader header = element;
// httpGet.setHeader(String.valueOf(header.get("key")), String.valueOf(header.get("value")));
//为了用例的执行功能,做了改动。但依然失败
httpGet.setHeader(String.valueOf(header.getKey()), String.valueOf(header.getValue()));
}
CloseableHttpResponse response = getGetResponse(httpClient, httpGet);
return getResult(response, encoding);
}
public static CloseableHttpResponse sendGetData(String url) {
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建get方式请求对象
HttpGet httpGet = new HttpGet(url);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000).setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
httpGet.setConfig(requestConfig);
// Content-type设置为application/x-www-form-urlencoded 或者不设置也是可以的(默认为application/x-www-form-urlencoded)
httpGet.addHeader("Content-type", "application/json");
// 通过请求对象获取响应对象
return getGetResponse(httpClient, httpGet);
}
public static CloseableHttpResponse getPostResponse(CloseableHttpClient httpClient, HttpPost httpPost) {
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
public static CloseableHttpResponse getGetResponse(CloseableHttpClient httpClient, HttpGet httpGet) {
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
public static HttpResponse getResult(CloseableHttpResponse response, String encoding) {
HttpResponse resp = new HttpResponse();
String res = "";
int statusCode = response.getStatusLine().getStatusCode();
List<HttpHeader> httpHttpHeaders = new ArrayList<>();
//if (statusCode == HttpStatus.SC_OK) {
try {
res = EntityUtils.toString(response.getEntity(), encoding);
resp.setResBody(res);
resp.setResCode(statusCode);
org.apache.http.Header[] allHeaders = response.getAllHeaders();
for (org.apache.http.Header header : allHeaders) {
String name = header.getName();
String value = header.getValue();
httpHttpHeaders.add(new HttpHeader(name, value));
}
resp.setResHeaders(httpHttpHeaders);
} catch (IOException e) {
e.printStackTrace();
return null;
}
//}
// 释放链接
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return resp;
}
}