|
4#
楼主 |
发表于 2022-8-9 15:41:02
|
只看该作者
本帖最后由 草帽路飞UU 于 2022-8-12 15:00 编辑
4.5.2 请求方法封装
// httpService.js
import request from 'request';
// 自动登录和获取token逻辑,在此不做赘述
import { webLogin } from './login';
// 用户账号和服务配置,在此不做赘述
import { serviceConfig } from '@/config/testConfig/paramsConfig';
// reporter 文件的详细配置请参考 4.5.1
import {
requestReporter,
paramsReporter,
expectReporter,
brokenTest
} from '@/utils/testUtils/reporter';
let cookies;
/**
* web 端对外暴露的 http 服务
* @param {string} url 接口
* @param {object} params 参数
* @param {string} method 方法,默认 post
* @returns {object} promise
*/
export function httpService(url, params, method = 'post') {
// 若无 token 则先走登录逻辑
if (!cookies) {
return webLogin(serviceConfig).then((res) => {
cookies = res;
return service(serviceConfig.server + url, params, method, cookies);
});
} else {
return service(serviceConfig.server + url, params, method, cookies);
}
}
/**
* http 服务实体,基于 request 模块封装
* @param {string} url 接口
* @param {object} params 参数
* @param {string} method 方法
* @param {string} cookies cookies
*/
function service(url, params, method, cookies) {
return new Promise((resolve, reject) => {
// request.debug = true;
// 封装 get 请求的 query 参数
let query = '';
if (requestMethod === 'get' && params) {
Object.keys(params).forEach((item, index) => {
query += index === 0 ? '?' : '&';
query += item + '=' + params[item];
});
}
// 有种 cookie 需要传对象结构,所以做了区分
const headers =
Object.prototype.toString.call(cookies) === '[object Object]'
? {
'Content-Type': 'application/json',
...cookies
}
: {
'Content-Type': 'application/json',
cookie: cookies
};
request(
{
url: url + query,
method: method,
headers: headers,
body: JSON.stringify(params)
},
function (error, res, body) {
let data = body;
// 尝试 json 格式转换
try {
data = JSON.parse(body);
} catch (err) {}
// 输出请求日志、参数信息
const requestInfo = JSON.stringify({ url, params, data });
requestReporter(requestInfo);
paramsReporter(params);
if (!error && res.statusCode === 200 && global.checkFailed) {
// 校验 success:false 接口
expectReporter('接口请求失败', () => {
expect(data.success).toBeFalsy();
});
reject(data);
} else if (!error && res.statusCode === 200 && !global.checkFailed) {
// 正常返回接口
expectReporter('接口请求成功', () => {
expect(data.success).toBeTruthy();
});
resolve(data.data);
} else if (error || res.statusCode !== 200) {
// 请求失败
brokenTest();
reject(data);
}
// 允许请求失败参数,用于校验异常情况的错误返回
global.checkFailed = false;
}
);
});
}
|
4.6 测试文件执行
4.6.1 进程文件
import inventoryService from '@/api/service/inventory';
// 通过校验规则获取随机值的方法,属于业务封装,在此不做赘述
import { getRandomDataByRules } from '@/utils/testUtils/methods';
// 校验必要字段的方法,属于业务封装,在此不做赘述
import { requiredDeclare } from '@/utils/testUtils/declare';
// reporter 文件的详细配置请参考 4.5.1
import { expectReporter } from '@/utils/testUtils/reporter';
// 数据层配置项请参考 4.4.2
import {
detailFields,
intoLocationValidate
} from '@/views/.../intoLocationFields';
/**
* 获取库存信息
* @param {object} orderInfo 订单信息
*/
export function getIntoLocationDetails(orderInfo) {
const params = {
orderId: orderInfo.orderId
};
return inventoryService.detail(params).then(
(res) => {
expectReporter('库存数量大于0', () => {
expect(res.count).toBeGreaterThan(0);
});
requiredDeclare(
detailFields(),
res
);
return res;
},
() => {}
);
}
/**
* 进行入库操作
* @param {object} detailData 订单详情
* @param {object} specifiedData 指定的入参数据,用于校验异常情况
*/
export function intoLocation(detailData, specifiedData = {}) {
const params = {
amount: detailData.orderAmount,
amountSave:
specifiedData.amountSave ||
getRandomDataByRules(intoLocationValidate.amountSave(detailData)),
orderId: detailData.orderId,
stockType: getRandomDataByRules(intoLocationValidate.stockType())
};
return inventoryService.add(params).then(
() => {
return params;
},
() => {}
);
}
|
4.6.2 用例文件
import {
getIntoLocationDetails,
intoLocation
} from './process/intoLocation';
// 生成订单、清除订单状态进程,涉及业务在此不做赘述
import { generateOrder, clearStatus } from './process/common';
// reporter 文件的详细配置请参考 4.5.1
import { expectReporter } from '@/utils/testUtils/reporter';
// describe、it 等变量的定义请参考 4.5.1
describe.custom('获取库存信息-进行入库操作-库存数量减少', (descData) => {
// 生成订单
beforeAll(() => {
return generateOrder().then((res) => {
descData.orderParams = res;
});
});
it.custom(
'库存信息字段正常,获取库存信息供入库使用',
() => {
return getIntoLocationDetails(descData.orderParams).then((res) => {
descData.detailData = res;
});
},
['orderParams'],
5,
['全部入库']
);
it.custom(
'全部入库',
() => {
return intoLocation(descData.detailData).then(
(res) => {
descData.intoLocationParams = res;
}
);
},
['detailData'],
5,
['全部入库']
);
it.custom(
'库存数量发生改变',
() => {
return getIntoLocationDetails(descData.orderParams).then((res) => {
expectReporter('库存数量=原库存数量+入库数量', () => {
expect(Number(res.inventoryAmount)).toEqual(
Number(descData.detailData.inventoryAmount) +
Number(descData.intoLocationParams.amountSave)
);
});
});
},
['orderParams', 'detailData', 'intoLocationParams'],
5,
['全部入库', '数据变化']
);
// 两次入库数量
let intoLocationCount = [1, 2];
it.eachCustom(intoLocationCount)(
'第%d次入库',
(count, index) => {
// global.checkFailed = true;
const specifiedData = {
amountSave: count
};
return intoLocation(
descData.detailData,
specifiedData
);
},
['detailData'],
5,
['两次部分入库']
);
// 结束测试后恢复订单状态,不恢复的话业务上会占用其他资源
afterAll(() => {
return clearStatus();
});
});
|
5.效果展示
5.1 测试报告
(由于涉及到部分公司业务,故部分文字打码处理,望见谅)
5.1.1 总览
5.1.2 测试结果分类
5.1.3测试用例展示
5.1.4 历史数据图表
5.1.5 测试时间轴
5.1.6 测试用例分组(根据场景)
5.2 测试覆盖率
5.2.1 表格总览
展示的为 API 层的文件统计
5.2.2 代码执行统计
6.总结
6.1 优势
与前端代码结合
投入产出比高,迭代成本低
测试层与业务层共用了 API 层和数据层,涉及到 API 和数据的改动,只需要改一处即可两边生效
直接在代码开发过程中,同步进行测试进程编写,然后根据用例对进程进行组合生成用例
相较于传统自动化测试,开发周期更短(有效精简人员)
难度可预见性
根据业务代码的开发经验可以有效预估脚本开发周期、开发难度
6.2 局限性
测试用例的输出专业性不足
数据层需要一套完善的方案来使测试层和业务层共用
最后
本来想搞个实际案例的 git 项目出来分享的,后来考虑到接口测试还要后端支持,光靠前端也跑不起来,就暂时搁置了。后面有时间的话再补吧(先立个 flag 在这里了┗( ▔, ▔ )┛)。
还有就是,要是上面案例中有什么不对的地方,或者有更好的解决方案的,欢迎指正!
|
|