走进Java接口测试之参数关联设计

举报
zuozewei 发表于 2021/12/29 10:09:28 2021/12/29
【摘要】 一般在接口测试中,除了单一接口测试外,还会存在串联接口测试。在串联接口测试中,会使用前一个接口的返回值做为下一个接口的入参。在这种情况下,如何来获取或管理这种参数呢? 做过性能测试的都知道“关联”这个名词,使用关联场景一般是在测试某一个接口时,它的入参并非是固定不变的值,而是动态生成的。当调用这个接口时,需要按照指定的规则生成这个参数值,而一般这种情况下的入参来自另一个接口的返回值。

前言

一般在接口测试中,除了单一接口测试外,还会存在串联接口测试。在串联接口测试中,会使用前一个接口的返回值做为下一个接口的入参。在这种情况下,如何来获取或管理这种参数呢?
做过性能测试的都知道“关联”这个名词,使用关联场景一般是在测试某一个接口时,它的入参并非是固定不变的值,而是动态生成的。当调用这个接口时,需要按照指定的规则生成这个参数值,而一般这种情况下的入参来自另一个接口的返回值。

常见的场景例如:OAuth 2.0 授权登录,电商下单等。

示例场景

这里我们以 OAuth 2.0 授权登录场景做示例。

OAuth 2.0 授权登录

OAuth 2.0 密码模式授权原理图:

OAuth 2.0 密码模式授权原理图

主要流程:

  1. 客户端从用户获取用户名和密码;
  2. 客户端通过用户的用户名和密码访问认证服务器;
  3. 认证服务器返回访问令牌(有需要带上刷新令牌)。

两个示例接口

会员登录接口(通过用户名密码获取 token):

在这里插入图片描述

在这里插入图片描述

获取会员信息接口(header 带上 token 请求会员信息):

在这里插入图片描述

在这里插入图片描述

Hutool 单例工具 Singleton

平常我们使用单例不外乎两种方式:

  • 在对象里加个静态方法getInstance()来获取,可分为饿汉和饱汉模式。
  • 通过 Spring 这类容器统一管理对象,用的时候去对象池中拿。Spring也可以通过配置决定懒汉或者饿汉模式

说实话我更倾向于第二种,但是 Spring 更注重的是注入,而不是拿,而 Hutool 提供 Singleton 这个工具类,维护一个单例的池,用这个单例对象的时候直接来拿就可以,其使用的懒汉模式,这样管理单例的是一个容器工具,而不是一个大大的框架,这样能大大减少单例使用的复杂性。

Singleton 工具类源码如下:

/**
 * 单例类<br>
 * 提供单例对象的统一管理,当调用get方法时,如果对象池中存在此对象,返回此对象,否则创建新对象返回<br>
 *
 * @author loolly
 */
public final class Singleton {

	private static final SimpleCache<String, Object> POOL = new SimpleCache<>(new HashMap<>());

	private Singleton() {
	}

	/**
	 * 获得指定类的单例对象<br>
	 * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象<br>
	 * 注意:单例针对的是类和参数,也就是说只有类、参数一致才会返回同一个对象
	 *
	 * @param <T>    单例对象类型
	 * @param clazz  类
	 * @param params 构造方法参数
	 * @return 单例对象
	 */
	public static <T> T get(Class<T> clazz, Object... params) {
		Assert.notNull(clazz, "Class must be not null !");
		final String key = buildKey(clazz.getName(), params);
		return get(key, () -> ReflectUtil.newInstance(clazz, params));
	}

	/**
	 * 获得指定类的单例对象<br>
	 * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象<br>
	 * 注意:单例针对的是类和参数,也就是说只有类、参数一致才会返回同一个对象
	 *
	 * @param <T>      单例对象类型
	 * @param key      自定义键
	 * @param supplier 单例对象的创建函数
	 * @return 单例对象
	 * @since 5.3.3
	 */
	@SuppressWarnings("unchecked")
	public static <T> T get(String key, Func0<T> supplier) {
		return (T) POOL.get(key, supplier::call);
	}

	/**
	 * 获得指定类的单例对象<br>
	 * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象<br>
	 *
	 * @param <T>       单例对象类型
	 * @param className 类名
	 * @param params    构造参数
	 * @return 单例对象
	 */
	public static <T> T get(String className, Object... params) {
		Assert.notBlank(className, "Class name must be not blank !");
		final Class<T> clazz = ClassUtil.loadClass(className);
		return get(clazz, params);
	}

	/**
	 * 将已有对象放入单例中,其Class做为键
	 *
	 * @param obj 对象
	 * @since 4.0.7
	 */
	public static void put(Object obj) {
		Assert.notNull(obj, "Bean object must be not null !");
		put(obj.getClass().getName(), obj);
	}

	/**
	 * 将已有对象放入单例中,其Class做为键
	 *
	 * @param key 键
	 * @param obj 对象
	 * @since 5.3.3
	 */
	public static void put(String key, Object obj) {
		POOL.put(key, obj);
	}

	/**
	 * 移除指定Singleton对象
	 *
	 * @param clazz 类
	 */
	public static void remove(Class<?> clazz) {
		if (null != clazz) {
			remove(clazz.getName());
		}
	}

	/**
	 * 移除指定Singleton对象
	 *
	 * @param key 键
	 */
	public static void remove(String key) {
		POOL.remove(key);
	}

	/**
	 * 清除所有Singleton对象
	 */
	public static void destroy() {
		POOL.clear();
	}

	// ------------------------------------------------------------------------------------------- Private method start

	/**
	 * 构建key
	 *
	 * @param className 类名
	 * @param params    参数列表
	 * @return key
	 */
	private static String buildKey(String className, Object... params) {
		if (ArrayUtil.isEmpty(params)) {
			return className;
		}
		return StrUtil.format("{}#{}", className, ArrayUtil.join(params, "_"));
	}
	// ------------------------------------------------------------------------------------------- Private method end
}

大家如果有兴趣可以看下这个类,实现非常简单,一个 HashMap 用于做为单例对象池,通过 newInstance() 实例化对象(不支持带参数的构造方法),无论取还是创建对象都是线程安全的(在单例对象数量非常庞大且单例对象实例化非常耗时时可能会出现瓶颈),考虑到在 get 的时候使双重检查锁,但是并不是线程安全的,故直接加了 synchronized 做为修饰符,

实践过程

引包

集成 hutool 工具包,另外测试框架选用 testng:

<!--引入 hutool 工具类库-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.4.2</version>
</dependency>

<!--引入testng测试框架-->
<dependency>
	<groupId>org.testng</groupId>
	<artifactId>testng</artifactId>
	<version>6.14.3</version>
</dependency>

新建 AssicateParam 实体类

AssicateParam 提供一个数据存储实体:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AssicateParam {
	private String key;
	private String value;
}

用例开发

实现会员登录接口测试类:


/**
 * 描述:
 * 会员登录接口测试
 *
 * @author zuozewei
 * @create 2020-11-7 15:26
 */

@SpringBootTest
@Slf4j
public class TestCase1 extends AbstractTestNGSpringContextTests {

    private String host = "demo.7d.com";
    AssicateParam assicateParam = new AssicateParam();

    @Test(description = "登录")
    public void testLogin() {

        //构造参数
        String url = "/portal/sso/login";
        //将多个参数对加入到Map中
        Map<String, Object> map = MapUtil.newHashMap();
        map.put("username", "lisi");   //存储key和value
        map.put("password", "123456");

        //链式构建请求
        HttpResponse res = HttpRequest.post(host + url)
                .header(Header.CONTENT_TYPE, "application/x-www-form-urlencoded") //头信息,多个头信息多次调用此方法即可
                .form(map)   //表单内容
                .timeout(20000)//超时,毫秒
                .execute();

        //获取响应体
        String result = res.body();
        //获取响应码
        int status = res.getStatus();

        //断言
        Assert.assertEquals(200,status);
        Assert.assertNotNull(result);

        //打印
        log.info(JSONUtil.formatJsonStr(result));
        log.info(JSONUtil.parseObj(result).getByPath("data.token").toString());

        //存储token
        assicateParam.setKey("token");
        assicateParam.setValue(JSONUtil.parseObj(result).getByPath("data.token").toString());
        Singleton.put(assicateParam);
    }
}

实现获取会员信息接口测试类:

/**
 * 描述:
 * 获取会员信息接口测试
 *
 * @author zuozewei
 * @create 2020-11-7 15:26
 */

@SpringBootTest
@Slf4j
public class TestCase2 extends AbstractTestNGSpringContextTests {

    private String host = "demo.7d.com";

    @Test( description = "获取会员信息")
    public void testInfo() {
        AssicateParam assicateParam = Singleton.get(AssicateParam.class);

        //打印
        log.info(assicateParam.getKey() +" : "+ assicateParam.getValue());

        //构造参数
        String url = "/portal/sso/info";
        //关联参数
        String token = assicateParam.getValue();

        //链式构建请求
        HttpResponse res  = HttpRequest.get(host + url)
                .header(Header.CONTENT_TYPE, "application/x-www-form-urlencoded") //头信息,多个头信息多次调用此方法即可
                .header(Header.AUTHORIZATION,"Bearer " + token.toString())
                .timeout(20000)//超时,毫秒
                .execute();

        //获取响应体
        String result = res.body();
        //获取响应码
        int status = res.getStatus();

        //断言
        Assert.assertEquals(200,status);
        Assert.assertNotNull(result);

        //打印
        log.info(JSONUtil.formatJsonStr(result));
    }
}

参数关联的原理比较简单,引入 Hutool 单例工具 Singleton,把参数存入在实体中,可以通过 put 和 get 方法在用例里进行操作即可。

⚠️注意:

单例对象每次取出为同一个对象,除非调用 Singleton.destroy() 或者 remove 方法

跑测

新建一个 xml 测试用例集:

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="TestCase" verbose="1" preserve-order="true">
    <test name="TestCase" preserve-order="true">
        <classes>
            <class name="com.zuozewei.demo.example.TestCase1"/>
            <class name="com.zuozewei.demo.example.TestCase2"/>
        </classes>
    </test>
</suite>

执行后的结果如下:

会员登录接口

获取会员信息接口

✌️我们可以看到跨用例的参数关联已经实现。

小结

借用单例模式或借鉴其思想就可以快速解决接口测试中串联接口参数关联的问题。

示例demo:

参考资料:

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。