Puppeteer自动化测试实践四.覆盖率的生成

覆盖率是衡量自动化效果的一个重要指标。而在我们实现自动化测试覆盖率的时候遇到了一些问题,接下来简单讲一下怎么解决的。

一.覆盖率数据的收集

await page.coverage.startJSCoverage()
let jsCoverage = await page.coverage.stopJSCoverage()

 puppeteer的覆盖率数据收集两个方法,一个是在页面open时候开始统计覆盖率,另一个是页面关闭之前拿到这个覆盖率数据。coverage数据格式如下:

url是页面加载的js文件,text是js文件内容,ranges是覆盖率行信息。是一个带起始位置的数组。如下:

这里是一个页面的覆盖率数据。而我们的自动化目前有近100个用例,那就产生一百个覆盖率数据。然后又分别对应n个js文件的数据。所以自然而然的想到我们需要将这些数据按js文件合并。这样才能整合每个用例的覆盖率数据来生成汇总信息。

所以刚开始我以为在jest的global里面可以存储全局数据。比如PuppeteerEnvironment,或者一个全局单例来存储数据,但是通通不行。得出的结论是每个测试用例的运行环境是完全独立的。包括PuppeteerEnvironment,包括全局单例。所以jest是真正的多进程并发运行用例的。

内存缓存不行,那就只能使用文件缓存的。所以我们的实现方式是stopJSCoverage后,将coverage信息存储到文件。存储文件有个小技巧。因为我们是要按js文件来统计覆盖率的。所以我们还是要按js文件来存储覆盖率。首先把js文件url简单hash,作为文件目录名称。这里我们要存储url,text,range、所以分别新建对应文件和目录。

range里面存储每个页面的覆盖信息。这里之前遇到个小问题,在命名文件名的时候,刚开始用url hash + 时间戳来命名。后面发现jest的多进程下面同一个时间戳很有可能存在多个用例的写入,造成某个用例执行了却丢失了覆盖率。所以加上了随机数。range文件命名格式为newDate().getTime() + Math.floor(Math.random() * 10000)。

二.覆盖率数据的合并

保存覆盖率数据后,等待所有自动化用例执行结束,我们就需要来统计覆盖率数据了。最开始我以为range信息既然是数组。那我们简单的把多个range文件数组合并。生成一个大的range信息,传递给PuppeteerToIstanbul就可以了。这个实现是没什么问题。但是当我们运行起来后,单单覆盖率的生成大约需要20分钟,并且大概率出现OOM异常,造成node直接崩溃。所以我们的解决办法是range信息必须合并。干掉冗余信息。

1.)去重

在上面的截图中可以看到range信息是有序的增序排序的数组,而每个数组内容是start,end。所以我们可以定制一个二分查找。

/**
 * 有序的二分查找,返回-1或存在的数组下标。不使用递归实现。
 * @param target 要查找的数据
 * @param ranges_array 已经解析的range数组
 * @returns {*}
 */
function binarySearch(target, ranges_array) {
  let start = 0;
  let end = ranges_array.length - 1;

  while (start <= end) {
    let mid = parseInt(start + (end - start) / 2);
    const array_item = ranges_array[mid];
    if (target.start === array_item.start && target.end === array_item.end) {
      return mid
    } else if (target.start > array_item.end) {
      start = mid + 1
    } else if (target.end < array_item.start) {
      end = mid - 1
    } else {
      if (target.start <= array_item.end && target.end > array_item.end) {
        array_item.end = target.end;
        return mid
      } else if (target.end >= array_item.start && target.start < array_item.start) {
        array_item.start = target.start;
        return mid
      }
      break
    }
  }
  return -1
}

 

let url, ranges = [], text;
url = fs.readFileSync(temp_path + '/' + filename + '/url', 'utf8');
text = fs.readFileSync(temp_path + '/' + filename + '/text', 'utf8');
let sourceMapUrl = url + '.map';

const temp_range = JSON.parse(fs.readFileSync(temp_path + '/' + filename + '/range/' + sub_file, 'utf8'));
temp_range.forEach(range_item => {
const index = binarySearch(range_item, ranges);
if (index === -1) {
        ranges.push(range_item)
    }
})

 经过这一步之后,我们已经去重了绝大部分的range信息。但是问题又来了,可能有重叠的数据我们没有优化,并且上面的解析的push之后的数据已经不是有序的了。所以需要第二步的合并。这里为什么不一次合并重叠数据是因为,合并了一次之后,原始数据就已经不是有序的了。那就没办法使用快速二分查找,这样的效率相比于我的方法会大大降低。(重复覆盖率大概有80%左右,经过这一轮去重覆盖率数据大大降低,第一步首先去重是必要的)。

2).合并重叠区域和重新排序

经过上面的ranges.push(range_item)操作后,我们的覆盖率数据已经不是有序排列的了。并且覆盖率数据之间的重叠我们没有处理。

左边是包含关系,我们可以把下面的直接剔除掉。右边图是重叠关系。则可以用start1,end2来表示新的区间。

        let formatRangeMap = {};
        ranges.forEach(item => {
          let temp = formatRangeMap[item.start];
          if (temp) {//判断是否已经有对应的start位置。如果有,说明覆盖率数据是重合的。
            formatRangeMap[item.start] = {start: item.start, end: Math.max(item.end, temp.end)}
          } else {
            formatRangeMap[item.start] = item
          }
        });

先定义一个按start为key的map对象。如果start位置已经有值。则有重叠。end取对应的两者最大值。

        let formatRangeArray = [];
        const sortKeys = Object.keys(formatRangeMap).sort((a, b) => a - b);//从小到大排序
        sortKeys.forEach(key => {
          const target = formatRangeMap[key];//这里因为key值是从小到大排序的。所以新的target的start值是绝对大于等于formatRangeArray[length-1]的start的。所以只用判断最后一个
          if (formatRangeArray.length > 0) {
            const end_array = formatRangeArray[formatRangeArray.length - 1];
            if (target.start <= end_array.end) {
              end_array.end = Math.max(end_array.end, target.end)
            } else {
              formatRangeArray.push(target)
            }
          } else {
            formatRangeArray.push(target)
          }
        });
        console.log('ranges=', ranges.length, formatRangeArray.length);

 先将formatRangeMap的key值按从小到大排序。然后取对应的range内容,这里因为key值是从小到大排序的。所以拿到的target的start值是绝对大于等于formatRangeArray[length-1]的start的。所以只用判断最后一个。判断target的start是否小于end_array的end。如果是的话,则重新修改一下end_array的end值。这样已经解决完全包含问题。

 

三.解析sourcemap

webpack编译后的文件如果压缩混淆后,remap-istanbul无法解析正确的内容。所以我们在自动化测试使用webpack编译代码时候不要加代码的压缩混淆(不要用webpack-parallel-uglify-plugin)。并且需要生成对应的sourcemap。编译后生成的sourcemap文件和js文件在同一目录。sourcemap内容简单如下:

主要是sources数组字段,里面的文件名都是webpack开头的。所以我们需要把他替换成对应的src目录。

        const loadSourceMap = () => {
          return new Promise(resolve => {
            if (options.url_regexp.test(url)) {
              const module_path = RegExp.$1;
              const file_path = pathLib.resolve(options.source_map_dir, module_path + '.map');
              console.log('module_path=', module_path);
              fs.readFile(file_path, 'utf8', function (err, data) {
                if (err) {
                  resolve(null)
                } else {
                  resolve(data)
                }
              })
            } else {
              resolve(null)
            }
          })
        };
        const sourceMap = await loadSourceMap();
        const sourceMapObject = JSON.parse(sourceMap);
        if (sourceMapObject) {
          sourceMapObject.sources = sourceMapObject.sources.map(url => {
            console.log('src_dir=', url.replace('webpack:///./src', options.src_dir));
            return url.replace('webpack:///./src', options.src_dir)
          })
        }

 options.url_regexp是需要加载js的sourcemap的一个正则表达式。因为有些无关js文件我们并不想生成覆盖率数据。

options.src_dir是我们代码目录。这样替换成一个可直接打开的源码目录。

allCoverage.push({url, ranges: formatRangeArray, text, sourceMapUrl, sourceMap: JSON.stringify(sourceMapObject)})

 最终生成的中间层数据格式为url,ranges,text,sourcemapurl,sourcemap;

 

 

四.转化生成nyc能识别的数据

转化istanbul参考代码https://github.com/istanbuljs/puppeteer-to-istanbul。它的问题是在于生成的覆盖率是编译之后的js文件的覆盖率。而不是我们源代码的覆盖率。这种覆盖率对我们来说只是个数字。而对我们如何提高覆盖率如何看源代码是没有帮助的。

所以我们要结合remap-istanbul来使用。这个是将Istanbul代码覆盖率信息通过sourcemap重新映射到其原代码位置。(一般我们不传递sourcemap地址给remap-istanbul,是因为它默认加载js文件同级目录的.map文件)

核心代码如下:

    writeIstanbulFormat() {
    var fullJson = {};

    this.puppeteerToV8Info.forEach(jsFile => {
      const script = v8toIstanbul(jsFile.url);
      script.applyCoverage(jsFile.functions);

      let istanbulCoverage = script.toIstanbul();
      let keys = Object.keys(istanbulCoverage);
      fullJson[keys[0]] = istanbulCoverage[keys[0]]
    });

    mkdirp.sync(this.options.nyc_output_path);
    const path = pathLib.resolve(this.options.nyc_output_path,  'out.json')
    console.log('path=', path);

    fs.writeFileSync(path, JSON.stringify(fullJson), 'utf8');

    remapIstanbul(path, {
      'json': path,
    });

    const text = JSON.parse(fs.readFileSync(path, 'utf8'));
    
    Object.keys(text).map(key => {
      const coverage_item = text[key];
      const path = coverage_item['path'];
      if (fs.existsSync(path) && (!this.options.filterCoverageFile || !this.options.filterCoverageFile(path))) {

      } else {
        delete text[key]
      }
    });
    fs.writeFileSync(path, JSON.stringify(text), 'utf8');
  }

第一部分是将覆盖率文件通过v8-to-istanbul将v8 coverage转化成istanbul的json格式文件存储到nyc_output_path

第二部分是通过remapIstanbul转化成源代码的覆盖率信息。

第三部分是过滤一下部分文件的覆盖率。因为有些js文件或者css文件,可以剔除其覆盖率。生成的最终out.json就是nyc能识别的覆盖率数据。

 

五.生成覆盖率

上面最终生成的/.nyc_output/out.json,这个文件就是Istanbul需要的覆盖率源文件。

这个时候我们调用脚本就可以生成html形式的html。

nyc report --reporter=html

 html内容如图:

查看具体文件覆盖信息:

 

 

生成简单缩略信息。

nyc report --reporter=text-summary

 

六.源码和使用方法

https://github.com/zhangzhige/puppeteer-coverage

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值