返回 blog
2024年4月24日
3 分钟阅读

代码覆盖率接入指南

什么是代码覆盖率

代码覆盖率指的是 相对所有源码行数,执行了的代码行数。 请勿把 「测试覆盖率」 跟 「代码覆盖率」混为一谈

为什么接入代码覆盖率,而非测试覆盖率

首先,「测试覆盖率」指的是测试用例相对于源码所覆盖到的行,通常来说,当一切测试覆盖率的基础搞定后,只需要一个命令即可得到测试覆盖率的结果,这是一种偏自动化的测试方式,接入难度也更高 而「代码覆盖率」,不需要我们编写测试用例,只是通过编译代码的时候,在源码中动态插入一些「能知道函数或变量是否被使用了」的代码(这个行为叫做 instrument 插桩)。然后测试同学通过手动在网页中测试,经过点击等一系列交互来执行前端代码,从而得到执行了的代码行数,也就是覆盖率 image.png image.png

如何接入

概念

首先简单讲一下覆盖率涉及到的一些基础知识 我使用的 istanbul.js 来做的代码覆盖率,要生成一个覆盖率结果,需要客户端和服务端配合 客户端主要要做两个工作:

  1. 构建的时候插桩
  2. 提供一个上报的入口

服务端主要做:

  1. 接收前端上报的coverage覆盖率数据,然后通过 nyc(也就是istanbul) 生成对应的结果
  2. 持久化数据

不过站在前端的角度上,只需要考虑客户端的工作,然后跟测试同学打交道,测试同学会调用我提供的服务来生成覆盖率结果

vite插件式接入

为了尽量减少侵入性,以及方便业务方接入,我把覆盖率集成到了vite插件中,这种方式是最简单的 同时,这种方式也把上文说的客户端的 「两个工作」 都做了

import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { istanbulWidget } from 'vite-plugin-istanbul-widget'

export default defineConfig((env) => ({
  plugins: [
    react(),
    // 引入插件
    istanbulWidget({
      enabled: !(env.mode === 'production'),
      // istanbul-widget 控件配置
      istanbulWidgetConfig: {
        // 
        theme: 'dark',
        defaultPosition: {
          x: 20,
          y: 100,
        },
        plugin: {
          report: {
            async onReport(coverage: any, ...args: any[]) {
              // 调用上报方法
              // 这个需要测试方提供接口上报
              await window.__report(coverage, ...args)
            },
          },
          setting: {
            autoReport: false,
            onLeavePage: true,
            requireReporter: true,
            text: '!设置文案!',
          },
          buttonGroup: [
            {
              text: '自定义按钮',
              onClick(...args: any[]) {
                window.__customClick(...args)
              },
            },
          ],
        },
      },
      // 全量上报
      fullReport: true,
    }),
  ],
}))

上报举例

const toFormData = (obj) => {
  const formData = new FormData()
  Object.keys(obj).forEach((key) => {
    formData.append(key, obj[key])
  })
  return formData
}

async function report(coverage, params) {
  // 这个域名是测试方的
  const res = await fetch('测试方提供的接口uri', {
    body: toFormData({
      pl_id: 'whatever', // 产品线id,测试同学告知
      version_name: __GIT_COMMIT_ID__, // 全局变量,会通过vite插件注入
      owner: params.setting.reporter, // 上报人
      src_roots: '/app', // docker容器中的workdir
      cov_data: JSON.stringify(coverage), // 覆盖率数据
    }),
    method: 'POST',
  })
  if (!res.ok) {
    throw new Error('上报失败')
  }
}

window.__report = report

由于插件注入了一些全局变量,所以为了更好的typescript提示,我们还需要引入一下类型

{
  "compilerOptions": {
    "types": ["vite-plugin-istanbul-widget/client"]
  }
}

dockerfile

因为业务方需要我们提供项目的 git commit id,所以我们需要在docker中新增一个 git 的镜像。 很简单:

# 举个例而已
FROM node:18.17-alpine3.17

# 在基础镜像后执行
RUN apk add git

这个git的镜像可以接入到我们的基础镜像中,提升构建速度

全局样式修改

因为测试方需要全量的代码覆盖率,所以全局的样式污染需要更改:

body {
  padding-top: 16px;
  width: 100%;
}

更改为

import { useGlobalStyle } from '@minko-fe/react-hook'

useGlobalStyle('body', {
  paddingTop: '16px',
  width: '100%',
})

扩展式接入(进阶)

基础使用的话,掌握插件式接入即可,如果有更加个性化的需求,可以使用扩展式接入

import { Button } from 'istanbul-widget/components'
import { IstanbulWidget } from 'istanbul-widget'

// 自定义插件
function MyPlugin() {
  return <Button size={'sm'}>this is my Plugin</Button>
}

const myPlugin = new IstanbulWidget.IstanbulWidgetReactPlugin('my_plugin', 'My Plugin', MyPlugin)

// 给插件添加事件监听
myPlugin.event.on('init', () => {
  console.log('my plugin inited')
})

const istanbulWidget = new IstanbulWidget({
  defaultPosition: {
    x: -100,
    y: 100,
  },
  plugin: {
    report: {
      onReport(coverage) {
        console.log('上报', coverage)
        throw new Error('上报失败')
      },
    },
    setting: {
      requireReporter: true,
    },
    buttonGroup: [
      {
        text: '额外按钮 - 1',
        onClick() {
          console.log('1')
        },
      },
      {
        text: '额外按钮 - 2',
        onClick() {
          console.log('2')
        },
      },
    ],
  },
})

// 把自定义的插件添加到istanbulWidget控件中
istanbulWidget.addPlugin(myPlugin)