接口测试 [接口测试平台一期] 接口测试用例参数化方案

bauul · 2017年12月25日 · 最后由 Test之十年丶减半 回复于 2020年06月04日 · 5813 次阅读
本帖已被设为精华帖!

缘由

12 月 23 号去上海参加了论坛举办的活动,干货蛮多的,感觉别人家做的都好腻害,
其中 Lego 接口自动化测试印象挺深的,通过配置文件的方式生成接口测试用例,
因为恰好我们公司也准备做接口自动化的部分,之前还没有这部分😅

依赖

  1. rest-assured
  2. testng

两种方案

代码实现

这是我在参加活动前的方案,有同事推荐 retrofit 不错,自己写了一下,感觉不方便
在论坛上看到的 rest-assured 挺不错的,用起来方便的

@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public abstract class BaseInterfaceTest {

    public static Map<String, String> testParametersMap = new HashMap<>();

    static {

        /** 公共参数 **/
        testParametersMap.put("appKey", "android_111");
        testParametersMap.put("appTimestamp", String.valueOf(System.currentTimeMillis()));
        testParametersMap.put("appTypeId", "0");
        testParametersMap.put("cookieId", "eee");
        testParametersMap.put("countryCode", "SA");
        testParametersMap.put("currency", "SAR");

    }

    @BeforeClass
    @Parameters({"baseUrl", "appVersion"})
    public void init(@Optional String baseUrl, @Optional String appVersion) {
        log.debug("init");

        if (baseUrl == null) {
            baseUrl= "http://weekly.test.com";
        }
        if (appVersion != null) {
            testParametersMap.put("appVersion", appVersion);
        }

        RestAssured.baseURI = baseUrl;
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
        RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);
    }

}
@Slf4j
@Listeners({InterfaceFailureHandle.class})
public class LoginNew extends BaseInterfaceTest{

    @Test
    public void loginWithUerNamePassword() {
        log.debug("actions");

        JSONObject jsonObject = new JSONObject();
        testParametersMap.put("userName", "carl@163.com");
        testParametersMap.put("password", "kkkkkk");
        jsonObject.putAll(testParametersMap);
        testParametersMap.put("sign", SignGen.getSign(jsonObject, SignGen.appSecret));
        jsonObject.putAll(testParametersMap);

        log.debug(jsonObject.toJSONString());

        given().body(jsonObject.toJSONString())
                .when().post("/user/login").then()
                .body("messageCode", is("0"),
                        "messageType", is(0));
    }
}

参数配置实现

简单学习了一下 yaml 配置文件的写法,还是没完全搞清楚,
所以我选择写出 javabean 出来先,然后 dump 出来,再按 dump 出来的格式学着具体写

@Data
public class APITestProject {

    private String baseUrl;
    private Map<String, Object> globalRequestParmeters = new HashMap<>();
    private Map<String, APITestSuite> testSuites = new HashMap<>();

}
@Data
public class APITestSuite {

    private String description;
    private List<APITestCase> testCaseList = new ArrayList<>();
    private Map<String, Object> testSuiteParameters = new HashMap<>();
}
@Data
public class APITestCase {

    private String name;
    private String description;
    private String apiUrl;
    private Map<String, String> sqlCommands = new HashMap<>();
    private Map<String, Object> requestParameters = new HashMap<>();
    private Map<String, Map<String, Object>> resultVerify = new HashMap<>();
}

一个 project 下面有多个 testsuite,一个 testsuite 下面有多个用例,然后全局参数,测试套参数,用例参数,校验结果

!!APITestProject
baseUrl: http://weekly.test.com
#全局参数
globalRequestParmeters:
  appVersion: '6.12'
  appTypeId: '0'
  countryCode: SA
  cookieId: e0dc298b-2e2d-4f14-ab80-cfcce4471679
  appKey: android_lk98f83
  currency: SAR
  lang: '0'
  appTimestamp: '1514204071822'
  terminalType: '1'
#测试套集合
testSuites:
  firstSuite:
    description: testSuiteDesc
    #测试用例集合
    testCaseList:
    - apiUrl: /user/login
      description: kkk
      name: loginNew
      requestParameters:
        password: kkkkkk
        userName: clark@163.com
      resultVerify:
        #结果校验
        is:
          messageType: 0
          messageCode: '0'
      #sql,在测试前或测试后做数据准备或还原,未完成
      sqlCommands: {}
    #测试套参数集合
    testSuiteParameters: {}
@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public class APITestExecutor {

    private static Map<String, Object> testParametersMap = new HashMap<>();
    private static APITestProject apiTestProject;

    static {

        Yaml yaml = new Yaml();
        try {
            apiTestProject = yaml.loadAs(new FileInputStream(new File("src\\main\\resources\\InterfaceTest.yaml")), APITestProject.class);
            testParametersMap.putAll(apiTestProject.getGlobalRequestParmeters());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.debug(e.getMessage());
        }
    }

    @BeforeMethod
    @Parameters({"baseUrl", "appVersion"})
    public void init(@Optional String baseUrl, @Optional String appVersion) {
        log.debug("init");

        if (baseUrl == null) {
            baseUrl = apiTestProject.getBaseUrl();
        }
        if (appVersion != null) {
            testParametersMap.put("appVersion", appVersion);
        }

        RestAssured.baseURI = baseUrl;
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
        RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);

    }

    @Test
    public void executor() {

        JSONObject jsonObject = new JSONObject();

        Map<String, APITestSuite> testSuites = apiTestProject.getTestSuites();
        for (Map.Entry<String, APITestSuite> entry : testSuites.entrySet()) {

            List<APITestCase> testCases = entry.getValue().getTestCaseList();
            for (APITestCase testCase : testCases) {
                testParametersMap.putAll(testCase.getRequestParameters());
                jsonObject.putAll(testParametersMap);

                testParametersMap.put("sign", SignGen.getSign(jsonObject, SignGen.appSecret));
                jsonObject.putAll(testParametersMap);

                log.debug(jsonObject.toJSONString());

                ValidatableResponse validatable = given().body(jsonObject.toJSONString()).when().post(testCase.getApiUrl()).then();

                Map<String, Map<String, Object>> resultVerify = testCase.getResultVerify();

                resultVerify.forEach((condition, v) -> {
                    v.forEach((key, value) -> {
                        try {
                            Matchers matches = new Matchers();
                            Matcher matcher = (Matcher) Matchers.class.getDeclaredMethod(condition, Object.class).invoke(matches, value);

                            validatable.body(key, matcher);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        } catch (NoSuchMethodException e) {
                            e.printStackTrace();
                        }

                    });
                });

                validatable.log().all();

            }
        }

    }

}

读取 yaml 文件并遍历,执行测试

对比

代码:

  • 优点是写代码,灵活性高,特殊场景什么的都可以处理;
  • 缺点:如果 100,1000 条用例的话,重复代码非常多,后期维护可能很心累

配置文件:

  • 优点是结构清晰,轻量级
  • 缺点:未来可能出现特殊场景,随着用例数上去之后,很难通过修改配置文件的方式去兼容更多的场景

问题

  1. 如果 yaml 文件中有 10 条用例,在执行时是放在一个@Test方法中执行的,就是说第 3 条用例失败即全部失败了,如何进一步拆分
  2. rest-assured 初始化需要 7~8 秒的时间,感觉有点长了

解决方案

  1. 通过代码来运行 testNG 的测试用例,读取用例后,放入队列中,每次执行时读取一条用例即可

    public class APITestRun {
    
    public static void main(String[] args) {
        for (int i=0; i<APITestUtils.getBlockingQueue().size(); i++) {
            TestNG testNG = new TestNG();
            testNG.setTestClasses(new Class[]{APITestExecutor.class});
            testNG.run();
        }
    
    }
    }
    
@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public class APITestExecutor {

    private APITestCase testCase;

    @BeforeMethod
    @Parameters({"baseURL"})
    public void init(@Optional String baseURL) {
        log.debug("init");

        if (baseURL == null) {
            baseURL = APITestUtils.getApiTestProject().getBaseURL();
        }

        RestAssured.baseURI = baseURL;
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
        RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);

        testCase = APITestUtils.getTestCase();
    }

    @Test
    public void executor() {

        JSONObject jsonObject = new JSONObject();
        jsonObject.putAll(APITestUtils.getApiTestProject().getGlobalRequestParmeters());
        jsonObject.putAll(testCase.getRequestParameters());

        String sign = SignGenerate.getSign(jsonObject, SignGenerate.appSecret);
        jsonObject.put("sign", sign);

        log.debug(jsonObject.toJSONString());

        ValidatableResponse validatableResponse = given().body(jsonObject.toJSONString()).when().post(testCase.getApiUrl()).then();

        Map<String, Map<String, Object>> resultVerify = testCase.getResponseVerify();

        resultVerify.forEach((condition, v) -> {
            v.forEach((key, value) -> {
                try {
                    Matchers matchers = new Matchers();
                    Matcher matcher = (Matcher) Matchers.class.getDeclaredMethod(condition, Object.class).invoke(matchers, value);
                    validatableResponse.body(key, matcher);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        });

        validatableResponse.log().all();

        updateTestCaseInfo();
    }

    /**
     * 更新测试用例的名字和描述信息
     */
    public void updateTestCaseInfo() {
        TestCaseEvent testCaseEvent = new TestCaseEvent() {
            @Override
            public void process(TestCaseResult testCaseResult) {
                log.debug("name:" + APITestUtils.getCurrentTestCase().getName());
                log.debug("description:" + APITestUtils.getCurrentTestCase().getDescription());
                testCaseResult.setName(APITestUtils.getCurrentTestCase().getName());
                ru.yandex.qatools.allure.model.Description description = new ru.yandex.qatools.allure.model.Description();
                description.setValue(APITestUtils.getCurrentTestCase().getDescription());
                testCaseResult.setDescription(description);
//                testCaseResult.getLabels().add(new Label().withName("testSuite").withValue(""));
            }
        };

        APITestUtils.getAllure().fire(testCaseEvent);
    }
}
@Slf4j
public final class APITestUtils {

    private static APITestProject apiTestProject;
    private static BlockingQueue<APITestCase> blockingQueue = new LinkedBlockingQueue<>();
    private static APITestCase currentTestCase;
    private static Allure allure = Allure.LIFECYCLE;

    private APITestUtils() {

    }

    static {
        Yaml yaml = new Yaml();
        try {
            // 加载用例文件
            apiTestProject = yaml.loadAs(new FileInputStream(new File("src\\main\\resources\\InterfaceTest.yaml")), APITestProject.class);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.debug(e.getMessage());
        }

        /** 遍历用例并放入队例中 **/
        Map<String, APITestSuite> testSuites = apiTestProject.getTestSuites();
        for (Map.Entry<String, APITestSuite> entry : testSuites.entrySet()) {
            List<APITestCase> testCases = entry.getValue().getTestCaseList();
            blockingQueue.addAll(testCases);
        }
    }

    public static BlockingQueue<APITestCase> getBlockingQueue() {
        return blockingQueue;
    }

    public static APITestCase getTestCase() {
        currentTestCase = blockingQueue.poll();
        return currentTestCase;
    }

    public static APITestCase getCurrentTestCase() {
        return currentTestCase;
    }

    public static APITestProject getApiTestProject() {
        return apiTestProject;
    }

    public static Allure getAllure() {
        return allure;
    }

报告

其他小芝麻

  1. TestNG 测试注解以及生命周期:
    @BeforeClass(执行一次)
    @BeforeMethod(N 个 Test 方法执行 N 次)
    @Test Test 方法(此注解可能在类上表示多个,在方法表示一个)
    @AfterMethod(N 个 Test 方法执行 N 次)
    @AfterClass(执行一次)

  2. LinkedBlockingQueue
    poll: 若队列为空,返回 null。
    remove:若队列为空,抛出 NoSuchElementException 异常。
    take:若队列为空,发生阻塞,等待有元素。

  3. okhttp EOF Exception
    见=>https://blog.csdn.net/m_xiaoer/article/details/72858895

最后

看别人做的好漂亮,好厉害,自己动手尝试去做的时候,就感觉坑也不少的

附言 1  ·  2017年12月26日
  1. 更新了下代码,可以看到用例名和描述信息了,不过 Default feature 和 Default story 还没找到,后续再更新,基本需求完成
附言 2  ·  2017年12月26日

接下来的问题:

  1. 如何把参数传到注解中去?因为使用了 allure 报告,就比如用例名,用例描述,如何传递给@Description注解?
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 40 条回复 时间 点赞

我也试过用 yaml 维护接口参数,我的烦恼是编写不方便,写 json 习惯了,所以我用.json 文件维护

—— 来自 TesterHome 官方 安卓客户端

bauul #46 · 2017年12月26日 Author
CC 回复

胡总给我点经验啊😊

感觉这样去做的工作量会很大,为啥不在已有的 jmeter 基础上来做这个呀

战 神 回复

没怎么用过 jmeter,帮忙提供更多的信息啊,用 jmeter 怎么玩啊

bauul 回复

jmeter 相当强大啊,可以参数化,可以做服务端性能,可以做接口自动化,也有自带的 UI 界面,是很完善的接口测试框架&&工具呀,你这个参数化,在 jmeter 的 Beanshell 前置处理器里面去做 很方便的。。。

战 神 回复

还可以去看看官方提供 API 的各种 sampler,apache 都给你正好啦,而且性能足够完美

战 神 回复

嗯,我学学,谢谢

乐高么,美团点评那个,我跟他讨论半天。

bauul #39 · 2017年12月26日 Author
卡农Lucas 回复

bauul #10 · 2017年12月26日 Author

@Lihuazhang
可以把编辑预览菜单放到底下吗?这样字码完了(超过一屏),就在底下直接点击编辑或预览了,而现在的情况就是需要滚到最上面点击预览,不方便啊

bauul 回复

我大致是这样的写法,这里主要自动生成 case 和异常 case,正常 case 的数据源

bauul #36 · 2017年12月26日 Author
CC 回复

👍
主要涵盖哪些场景,有没有哪些场景无法覆盖?

#12 楼 @carl 这种接口主要针对的是单接口的健壮性及正常结果的验证,业务接口组合我是用另外的框架处理的

—— 来自 TesterHome 官方 安卓客户端

#12 楼 @carl 你可以看下论坛里面得精华帖,我记得有几篇针对接口框架,我感觉还是不错的

—— 来自 TesterHome 官方 安卓客户端

bauul #15 · 2017年12月26日 Author
CC 回复

嗯,我看了几个 rest-assured 写的,还有 jmeter 写的,可惜我还对 jmeter 不熟,后面学了再搞

思寒_seveniruby 将本帖设为了精华贴 12月26日 22:52

我是分享那个 Lego 接口测试解决方案的,看来影响力还不错啊~

bauul #29 · 2017年12月29日 Author
陈永达 回复

感谢,干货满满的分享👍

另外你标题写的 “参数化” 是 “拿用例的方式”,还是每个用例请求过程中,“参数需要替换” 的动作啊?

bauul #21 · 2017年12月29日 Author
陈永达 回复

我这个现在是 “拿用例的方式”,每个请求过程中,参数需要替换我记得那天你也有讲到的,但是我没明白主要是哪种场景

经你这么一问,我似乎想明白了,是同一个用例,但是有不同的参数,实现的测试目的可能不一样,所以需要把请求参数再参数化,是这样吗?

我原来的理解是,对于同一个用例,不同的参数请求,是通过复制多个用例来实现的。
后面会把这些参数放到数据库中,用户可以在浏览器中看到这个用例的详细参数,如果用例失败,可以在浏览器中修改参数,对前端页面不熟悉,还要去做原型设计,这方面有什么快速成型的建议吗?
感谢

bauul 回复

在选取用例部分,我当时说的是,使用 testNG 的 xml 配置文件,通过@Parameter的方式,将 sql 传到 TestNG 的脚本,来实现测试用例的选取。

在测试用例中,“参数化” 最主要解决的问题是:针对那些可能会失效、会变化、会被删、需要通过一些计算才能得到的参数,进行一些 “设计”,每次执行用例的时候,实时执行 “设计” 来生成参数,增加用例的健壮性。
是解决这样的问题的,毕竟这样的数据如果硬代码,只会常常让测试用例报错,增加维护成本。

“我原来的理解是,对于同一个用例,不同的参数请求,是通过复制多个用例来实现的。” 你理解的是对的,不用测请求目的,是复制多个用例来实现的,我的设计就是,一个用例就是单纯的一个系列的测试目的。用例是最基础的数据,你想要分类,可以在用例的上面来加一层逻辑的分类来实现,我个人觉得没必要在用例这个维度上做过多的设计,一个用例有 10 个测试目的,我觉得没什么意义,也不够清晰。

前端页面的话,我个人 java 比较熟 Servlet 感觉还蛮简单的,Python 也有很多容易上手的框架。

bauul 回复

我不太看网站,可能会回的不怎么及时,可以加我微信,你回了微信上告诉我下

bauul #24 · 2018年01月02日 Author
陈永达 回复

了解了,非常感谢👍

测试数据很多的时候,yaml 一条条写起来不会很麻烦吗。。

bauul #26 · 2018年01月08日 Author
cctodd 回复

做了 Har 文件(通过 Fiddler 录制出来的)自动转 yaml 用例了,根据项目做了定制,不用手写参数了

bauul 回复

参考楼主的方案我实践了下,用比较适合回归,比如稳定、简单一点的接口。
我是拿来在线上跑的,没有加入 sql 查询。

bauul #28 · 2018年01月26日 Author
cctodd 回复

也可以加入参数传递的功能,比如A接口依赖B接口的参数或B接口的返回值,坑已踩完,回头我发一下

大神,有空将 yaml 文件修改成 mysql ,你再发一文吧

bauul #18 · 2018年01月29日 Author
cooling 回复

哈哈,年后计划,二期工作

楼主,“如何把参数传到注解中去?因为使用了 allure 报告,就比如用例名,用例描述,如何传递给@Description注解?” 请问下这个问题是怎么解决的呢?

bauul #33 · 2018年05月04日 Author
y 回复
  Allure.LIFECYCLE.fire(new TestCaseEvent() {
    @Override
    public void process(TestCaseResult testCaseResult) {
        List<Parameter> parameterList = new ArrayList<>();
        Parameter request = new Parameter();
        request.setName("Your key");
        request.setValue(Your value);
        request.setKind(ParameterKind.ARGUMENT);

    }
});
bauul [接口测试平台二期] 批量数据支持 中提及了此贴 06月22日 11:15

你好,能给下最后 testng 示例的代码吗?多谢

bauul #12 · 2018年09月29日 Author
yueyawan 回复

啥?这个吗?

TestNG testNG = new TestNG(false);
testNG.setSuiteThreadPoolSize(1);
testNG.setThreadCount(1);
testNG.setTestClasses(new Class[]{TestExecutor.class});
JsonTestUtils.getTestCase();
testNG.run();
bauul 回复

嗯,我写的是从 mysql 读取用例数据,想参考下你的代码

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08
  1. 接口测试用例的运行是基于 TestNG 来管理、运行的吗?
  2. Web 前端新增一个接口测试时,框架是每次都需要新生成一个.java 文件来存储测试用例吗? 还是说写一个通用的 Java 用例模板,根据前端不同的参数来生成不同的用例?
  3. 测试用例运行时通过 TestNG 的 XML 配置文件来运行吗?那每一个用例都需要一个配置文件吗?
bauul #41 · 2019年05月07日 Author
shandongdong 回复
  1. 是基于 testNG 运行的,不过不是 testNG 管理用例,用例在一个 json 文件中
  2. 只有一个 java 文件,作为执行器存在,所有的用例都在 json 文件中
  3. 没有使用 testNG 的配置文件,运行方法:
TestNG testNG = new TestNG(false);
testNG.setSuiteThreadPoolSize(1);
testNG.setThreadCount(1);
testNG.setTestClasses(new Class[]{TestExecutor.class});
testNG.run();
bauul 回复
  1. 只有一个 java 文件,作为执行器存在,所有的用例都在 json 文件中。针对这点我有个疑问。 只有一个 java 文件作为执行器的话,那么多个测试用例的结果如何收集呢?比如一个 json 文件中存在 10 条测试用例,你的执行器只有一个,那么运行时应该只有一个@Test方法,10 条用例中如何记录成功了几条,失败了几条呢? 然后结果怎么自定义解析出来?
shandongdong 回复
for (int i=0; i<APITestUtils.getBlockingQueue().size(); i++) {
        TestNG testNG = new TestNG();
        testNG.setTestClasses(new Class[]{APITestExecutor.class});
        testNG.run();
    }

bauul 回复

你这里应该是执行吧。 我的意思是,执行结果的控制及统计。比如 testNG.run() 是运行了一条用例,那么这条用例执行结果的统计是在什么地方做的?

战 神 回复

我一直在纠结:①是继续用纯代码编写脚本、②还是用 jmeter 这种工具;
1、纯代码来写效率低,部分测试人员编码能力不足,都是因素,后期维护工作量越来越大;好处:编码模型比较自由,提升编码能力
2、jmeter 方便提取接口用于压力测试,多人协同维护会有冲突的情况吗?你有全面的分析对比文章描述吗

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册