大家好!今天开始我将通过《玩转自动化测试》系列和大家一起探讨和学习自动测试技术。因为议题覆盖面广且内容很多,所以我打算把《玩转自动化测试》做成一个持续更新、由浅入深的系列, 内容将涵盖唯品会在自动化测试实践中遇到的痛点以及对应的解决方案;同时也会和大家探讨唯品会自主开发的框架和平台, 如:AutoV(自动化测试框架), Vmock-Server(服务模拟), Vmock-Agent(跨进程JVM字节码增强)等的原理和实现细节。
本系列的主要目的是让大家对日常工作中用到的框架、工具和平台的架构、原理、实现有更深入的了解,从而进一步提升自动化编写效率和问题定位的能力。
说明:唯品会自动化实践以及相关的框架、工具和平台大多数都是用Java语言开发,所以本系列所有的代码和例子都是基于Java的。
开篇第一炮,诚意满满,先来一个干货热下身!
本篇文章和大家一起探讨一下怎么通过简单的黑科技在自动化用例里面玩转DNS解析。
相信大家编写自动化测试用例的时候都会涉及到接口调用、数据库、消息中间件、外部缓存、远程执行服务器命令等需要建立对外连接的操作。如果目标机器IP永远不变,我们当然可以把IP写死在用例里,但是实际上IP常常是变化的,特别是在容器化环境下IP更不是固定的,而且用例很可能是在要在多套环境下执行,例如功能环境、联调环境、staging环境等, 写死IP必然会增加用例维护成本和问题定位难度。
因此,我们建议是用例里面通过域名访问而不是写死IP。
如果测试用例中要通过域名访问,通常的做法是有以下2种:
1. 用例修改系统hosts文件,这样一个机器同时只能有一个用例实例执行,如果在Jenkins上一个slave同时执行多个job就会有hosts配置冲突的问题
2. 对于Http协议接口,我们可以设置代理的方式,例如httpclient可以通过RequestConfig.custom().setProxy(proxy)把请求指向对应的IP, 但是这种做法是跟所用的框架紧密相关,而且对于非HTTP协议无能为力。
试想一下,如果我们被测的是一个客户端应用,它和服务端之间通过HTTP协议交互,并且有一个断线自动重连的功能, 那么对于该客户端的自动化测试,我们可以怎么通过编码模拟网络断开从而验证重连功能呢? 大家可能第一时间想到的是改iptables呗,但是这种方案有2个问题:
1. iptables命令是linux的工具,windows默认并没有提供类似iptables的防火墙工具,所以只能在linux下跑;
2. 应用的DNS的解析后往往是有缓存的,当域名已经被解析了并被DNS缓存起来,改了iptables也不会马上生效。
1. 关于InetAddress类
在介绍我们解决方案前,先介绍一下关于java网络编程基础中的java.net.InetAddress类。
InetAddress类是Java包装用来表示IP地址的高级表示,几乎所有的Java网络相关的类都和它有关系,例如:serversocket,socket,URL,DataGramSocket,DataGRamPacket等。也就是说,凡是需要通过IP和对方建立连接的,无论是TCP还是UDP协议,底层基本都是通过InetAddress类进行操作的。
InetAddress本身是一个静态工厂类,通过本身提供的静态方法实例化,下面是InetAddress类中常用方法的一些说明:
2. 关于NameService接口
InetAddress除了封装了IP请求,同时内部也维护了一个NameService列表,
提供了DNS解析功能。
NameService是java中用于域名解析操作的接口,内部定义了2个方法,分别是:
InetAddress[] lookupAllHostAddr(String host) ——通过域名获取所有
InetAddress实例;
String getHostByAddr(byte[] ip)——通过ip的byte数组反向获取域名。
另外值得注意的是NameService是在sun.net.spi.nameservice包下,看包名可知NameService是一个spi,其实InetAdderss本身也提供了spi方式从加载自定义的NameService实例。
3. InetAddress源码解读
InetAddress本身代码较多,后面的例子只用到了DNS解析的功能,所以这里我们只分析一下DNS解析的部分代码。
1) 以下是初始化NameService列表的代码:
1. static {
2. // create the impl
3. impl = InetAddressImplFactory.create();
4.
5. // get name service if provided and requested
6. String provider = null;;
7. String propPrefix = "sun.net.spi.nameservice.provider.";
8. int n = 1;
9. nameServices = new ArrayList<NameService>();
10. provider = AccessController.doPrivileged(
11. new GetPropertyAction(propPrefix + n));
12. while (provider != null) {
13. NameService ns = createNSProvider(provider);
14. if (ns != null)
15. nameServices.add(ns);
16.
17. n++;
18. provider = AccessController.doPrivileged(
19. new GetPropertyAction(propPrefix + n));
20. }
21.
22. // if not designate any name services provider,
23. // create a default one
24. if (nameServices.size() == 0) {
25. NameService ns = createNSProvider("default");
26. nameServices.add(ns);
27. }
28. }
可以看到NameService是InetAddress内部维护的一个静态列表,初始化时先判断是否有SPI定义的自定义NameService实现,如果没有则取默认实现。
2) 以下是DNS解析缓存的代码:
1. public CacheEntry get(String host) {
2. int policy = getPolicy();
3. //如果不使用dns缓存的ttl是0(networkaddress.cache.ttl变量),则不使用DNS缓存,每次都解析DNS
4. if (policy == InetAddressCachePolicy.NEVER) {
5. return null;
6. }
7. CacheEntry entry = cache.get(host);
8.
9. // check if entry has expired
10. if (entry != null && policy != InetAddressCachePolicy.FOREVER) {
11. if (entry.expiration >= 0 &&
12. entry.expiration < System.currentTimeMillis()) {
13. cache.remove(host);
14. entry = null;
15. }
16. }
17.
18. return entry;
19. }
可以看到DNS是缓存在InetAddress内部,其中InetAddressCachePolicy.NEVER表示不使用缓存。
4. 关于dnsjava
dnsjava是Java中的DNS实现,支持各种记录类型的DNS操作,需要额外依赖dnsjava.jar包。dnsjava提供了比InetAddress更丰富的DNS操作, 例如可以作为一个小型的DNS服务器,或者指定Name Server等。
本文方案虽然和dnsjava没多大关系,但是因为例子中有涉及到把通过dnsjava指定的Name Sever的功能, 所以简答提一下,具体用法这里不详细介绍, 需了解可参考http://www.dnsjava.org/。
1. 方案介绍
有了以前面的基础知识介绍,相信大家已经有点头绪了。其实方案非常简单:
1)提供一个自定义的NameService实现代替系统默认的实现;
2)设置为不使用DNS缓存。
2. 具体实现
1) 首先我们要新建一个自定义的NameService实现LocalManagedDnsProxy类。
代码如下:
1. @Override
2. public InetAddress[] lookupAllHostAddr(String name) throws UnknownHostException {
3. InetAddress ipAddresses = instance.get(name);
4. //缓存没有则使用默认的dns解析
5. if (ipAddresses == null) {
6. // defaultDnsImpl是LocalManagedDnsImpl实例
7. return defaultDnsImpl.lookupAllHostAddr(name);
8. }
9. String hostAddress = ipAddresses.getHostAddress();
10. return new InetAddress[]{ipAddresses};
11.
12. }
其中NameStore中缓存的是自动化测试中自定义的DNS解析记录,该实现类从NameStore的缓存中读取DNS解析。如果缓存中没有对应记录,则使用默认的DNS解析服务(这里是通过dnsjava来解析,后面会介绍到)
2) NameStore缓存类
NameStore类其实就是一个key为域名和value为InetAddress的Map, 用户可以按用例需要通过put或remove方法操作自定义的DNS解析记录,代码如下:
1. private final Map<String, InetAddress> globalNames;
2.
3. public void remove(String hostName) {
4. globalNames.remove(hostName);
5. }
6.
7. public InetAddress get(String hostName) {
8. return globalNames.get(hostName);
9. }
10.
11. public void put(String hostName, String ipAddress) {
12.
13. try {
14. InetAddress ipAddresses = InetAddress.getByAddress(DnsUtils.textToNumericFormat(ipAddress));
15. globalNames.put(hostName, ipAddresses);
16. } catch (UnknownHostException ignored) {
17. try {
18. Constructor constructor = Inet4Address.class.getDeclaredConstructor(new Class[]{String.class,
19. byte[].class});
20. constructor.setAccessible(true);
21. globalNames.put(hostName, (Inet4Address) constructor.newInstance(hostName,
22. IPAddressUtil.textToNumericFormatV4(ipAddress)));
23. } catch (Exception e) {
24. noOp();
25. }
26. }
27. }
3) 通过dnsjava设置Name Server和默认的DNS解析服务
如果NameStore没有对应缓存记录则直接使用系统默认DNS解析的话,其实不需要通过dnsjava额外多一层解析服务,但是AutoV中因为要动态指定外部Name Server地址, 所以需要dnsjava作为默认的解析服务。以下代码是LocalManageDnsImpl类通过dnsjava的API来做DNS解析:
1. public class LocalManagedDnsImpl implements NameService {
2.
3. @Override
4. public InetAddress[] lookupAllHostAddr(String name) throws UnknownHostException {
5. try {
6. final Lookup lookup = new Lookup(name, Type.A);
7. Record[] records = lookup.run();
8. //没有匹配记录则报UnknownHostException,走系统默认DNS解析
9. if (records == null) {
10. throw new UnknownHostException(name);
11. }
12.
13. InetAddress[] array = new InetAddress[records.length];
14. for (int i = 0; i < records.length; i++) {
15. ARecord a = (ARecord) records[i];
16. array[i] = a.getAddress();
17. }
18. return array;
19. } catch (TextParseException e) {
20. throw new UnknownHostException(e.getMessage());
21. }
22. }
以下代码是通过dnsjava设置外部的NameServer地址, 其中nameServer地址是通过Pandora(Pandora是唯品会的轻量级Paas平台,后续文章会涉及到)接口获取的:
1. SimpleResolver resolver = new SimpleResolver(nameServer);
2. Lookup.setDefaultResolver(resolver);
4) 加入自定义DNS解析服务
虽然InetAddress本身支持SPI方式指定自定义的NameService实现,但是AutoV作为公用的自动化测试框架希望提供一定的扩展性而不希望写死DNS实现方式, 所以是在框架代码里通过反射方式注入一个NameService自定义实现实现,以下是部分实现代码:
1. protected void setNameServiceReflectively(){
2.
3. List<NameService> nameServices = null;
4. try {
5. nameServices = (List<NameService>) ReflectionUtils.getStaticFieldValue(InetAddress.class,
6. "nameServices");
7. } catch (Exception e) {
8. Exceptions.checked(e);
9. }
10. if (nameServices != null) {
11. //仅仅添加Dns解析服务到解析链中,并作为优先,保留原来的解析服务
12. nameServices.add(0, new LocalManagedDnsProxy());
13. }
14. }
5) 设置不使用DNS缓存
Java提供了两个方式修改DNS缓存时间:
A. 在java.security文件设置networkaddress.cache.ttl=0
B. 代码开始前java.security.Security.setProperty("networkaddress.cache.ttl", “0”);
其中如果把ttl的值设置为0表示不使用DNS缓存。对于方式A需要改java配置文件全局生效不太好,对于B能生效前提是InetAddressCachePolicy类中的cahePolicy还没有初始化前执行,但是这个不好控制,如logback在初始化时就调了InetAddress.getLocalHost()导致用例代码设置的ttl时间不生效。
因此,AutoV中还是采用反射的方式动态修改cahePolicy, 代码如下:
1. private void setCachePolicyReflectively(){
2. try{
3. ReflectionUtils.setStaticFieldValue(InetAddressCachePolicy.class, "cachePolicy",0);
4. } catch (Exception e){
5. logger.error("failed to set cachePolicy", e);
6. }
7. }
万事俱备只欠东风,代码已经好了,现在我们来测试一下动态修改DNS是否生效。测试代码如下:
1. @Test
2. public void testChangeDns() throws UnknownHostException {
3. // 添加自定义DNS解析,让域名解析
4. NameStore.getInstance().put("www.baidu.com","133.3.33.33");
5. InetAddress inetAddress=InetAddress.getByName("www.baidu.com");
6. System.out.println("wrong address: "+inetAddress);
7. // 移除自定义域名解析
8. NameStore.getInstance().remove("www.baidu.com");
9. inetAddress=InetAddress.getByName("www.baidu.com");
10. System.out.println("original address: "+inetAddress);
11. }
可以看到控制台打印如下:
wrong address: /133.3.33.33
original address:
www.baidu.com/14.215.177.39
很明显,我们黑掉的DNS解析已经生效了,到这里我们可以在自动化用例里面玩转DNS解析了!
1. 本文演示了如何通过反射方式动态修改JAVA中默认的DNS解析方式和DNS缓存方式。
2. 有了动态DNS操作,我们在自动化用例中,只要是同一个JVM进程中域名的用例,无论哪种连接方式,都可以通过动态DNS的方式根据用例需要把请求指向任意IP。
3. 建议自动化项目尽量不要写死IP而是通过域名访问。实际上在AutoV中,自动化代码都是通过项目中不同profile目录下的hosts文件, 这样在不同环境中只需要指定不同的profile执行即可,既方便管理和维护、保证了自动化用例的平台和框架无关性,也解决了多个jenkins job并发执行时DNS解析冲突的问题。