第4.2节 Android App生成追溯关系

发布于:2025-07-08 ⋅ 阅读:(14) ⋅ 点赞:(0)
     前一节介绍了追溯关系的作用,现在落实到如何创建追溯关系。具体到一个App,就是需要知道一个用例执行过程中,它会覆盖哪些代码?创建用例与代码间的关系,保存下来就是追溯关系。那如何记录用例执行覆盖的代码呢?

4.2.1 覆盖率生成追溯关系

     移动端的覆盖率通过控制App进入前端和后端进行生成的,要关联用例,首先要控制用例执行过程中覆盖率的生成,也就是执行完一个用例,生成一次覆盖率数据文件,同时将用例信息与覆盖率信息传递给精准测试平台,标注这个覆盖率数据文件是这个用例执行时产生的。
      如果要向App传递信息,如用例ID,则需要通过一个技术:deeplink,通过调用deeplink链接,将CaseID接到链接中,app进行处理即可。如果不了解这个技术,请自行搜索,此处不再展开介绍。所以关联用例流程如下所示:
流程介绍:
  • 在覆盖率SDK中添加关闻用例弹层,用于展示用例信息,控制用例执行过程中的覆盖率采集。
  • 当单击弹层上的开始关联用例时,将用例ID传给App,同时清除原来的覆盖率数据,采集用例的覆盖率数据;
  • 执行手工测试用例,执行过程中关联用例弹层显示,用例关联中;
  • 用例执行完成后,单击结束关联用例,上传覆盖率文件和用例ID,实现关联用例操作。
  • 如果是自动化用例,通过自动化测试脚本在setup函数中调用deeplink传递用例信息,但不弹出关联用例弹层;
  • 结束用例执行时,在teardown函数中,执行结束关联用例操作。
注意:如果你的测试用例平台无法与手机发送deeplink命令,也可以通过接口来进行操作,在覆盖率SDK中调用测试用例管理平台接口,获取用例信息。

4.2.2 关联用例弹层开发

     关联用例时候,最重要的是要准确记录用例开始执行和结束执行的时机,而且端上的用例有很多前置操作。如要测试设置相关的操作,需要打开应用,登录帐号,进入设置页,然后才能进行具体的测试操作。而前面的很多步骤都是前置操作,按正常用例与代码关联的逻辑,这些操作是不能关联到这个用例上的,否则就容易过多评估用例的覆盖范围,在用例推荐环节也就不准确,容易多推荐用例。
如何解决用例关联时前置操作的问题?
一,需要能准确告诉覆盖率SDK何时开始采集覆盖率数据,何时结束采集覆盖率数据;
二,关联用例时找业务熟练的同学,准确识别出哪些操作步骤是关键步骤。

1,添加关联用例弹层

     为了达到准确定位关联用例的操作,对测试App添加如下弹层,这个弹层要显示在被测试应用所有页面之上,除非杀死应用,否则不会消失。
  • 被应用添加了关联用例弹层后,通过deeplink请求将用例ID传给应用,弹层显示出用例信息。
  • 用例信息为用例ID,每次deeplink请求,用例ID会随之而变化;
  • 当用户操作了前置步骤,进入关键步骤时,单击开始录制按钮,开始采集覆盖率数据,录制按钮变成停止按钮;
  • 当用例操作执行完成后,单击结束录制按钮,停止覆盖率的采集,并将覆盖率数据文件,用例ID和高力信息上传到精准测试平台,完成用例关联。
  • 开始关联下一个用例时,用例ID变化,重新走关联用例操作即可。

2,弹层设计

弹层layout布局代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:background="@null">
    <LinearLayout
        android:id="@+id/linear_cen"
        android:layout_width="280dp"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="20dp"
        android:layout_marginHorizontal="18dp"
        android:background="@drawable/bg_dialog">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/textView4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="16dp"
                android:layout_marginRight="10dp"
                android:layout_marginBottom="8dp"
                android:text="关联用例:"
                android:textColor="#FFFFFF"
                android:textSize="16sp"/>
            <ImageView
                android:id="@+id/btn"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_centerVertical="true"
                android:layout_marginHorizontal="9dp"
                android:layout_marginTop="11dp"
                android:layout_marginBottom="8dp"
                android:layout_alignParentRight="true"
                android:src="@drawable/ic_start"/>
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toRightOf="@+id/textView4"
                android:layout_toLeftOf="@+id/iv_refresh">
                <EditText
                    android:id="@+id/testcase"
                    android:hint="用例信息"
                    android:inputType="text"
                    android:layout_width="120dp"
                    android:layout_height="40dp"
                    android:textSize="16sp"
                    />
            </RelativeLayout>

        </RelativeLayout>


    </LinearLayout>
</RelativeLayout>

再添加上弹层的显示控制,移动控制等相关操作,注意对弹层上元素的操作添加上相关控制函数。
弹层移动控制类:
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.customview.widget.ViewDragHelper

const val TAG = "FloatView"

/**
 * 记录滑动过的最后的位置
 */

var lastX = -1f
var lastY = -1f

class DragViewParent(context: Context) : FrameLayout(context) {
    private lateinit var dragHelper: ViewDragHelper

    private lateinit var dragView: View
    private val viewWidth = 0
    private val viewHeight = 0

    fun setDragViewChild(view: View, width: Float, height: Float) {
        isClickable = false
        dragView = view

        var layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.WRAP_CONTENT,
            FrameLayout.LayoutParams.WRAP_CONTENT
        )
        addView(view, layoutParams)
        setViewDragHelper()
    }

    private fun setViewDragHelper() {
        dragHelper = ViewDragHelper.create(this, 1.0f, object : ViewDragHelper.Callback() {


            override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
                val leftBound = paddingLeft
                val rightBound: Int = width - dragView.width - leftBound
                return Math.min(Math.max(left, leftBound), rightBound)
            }

            override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
                val leftBound = paddingTop
                val rightBound: Int = height - dragView.height - dragView.paddingBottom
                return Math.min(Math.max(top, leftBound), rightBound)
            }


            //在边界拖动时回调
            override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {}

            override fun getViewHorizontalDragRange(child: View): Int {
                return measuredWidth - child.measuredWidth
            }

            override fun getViewVerticalDragRange(child: View): Int {
                return measuredHeight - child.measuredHeight
            }

            override fun tryCaptureView(child: View, pointerId: Int): Boolean {
                return dragView === child
            }


            override fun onViewPositionChanged(
                changedView: View,
                left: Int,
                top: Int,
                dx: Int,
                dy: Int
            ) {
                super.onViewPositionChanged(changedView, left, top, dx, dy)
                lastX = changedView.x
                lastY = changedView.y
            }
        })
        dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        return dragHelper.shouldInterceptTouchEvent(event)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        dragHelper.processTouchEvent(event)
        return true
    }

    override fun computeScroll() {
        if (dragHelper.continueSettling(true)) {
            invalidate()
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        restorePosition()

    }

    private fun restorePosition() {
        if (lastX == -1f && lastY == -1f) { // 初始位置
            lastX = (measuredWidth-dragView.measuredWidth) * FloatViewManger.proportionX
            lastY = (measuredHeight-dragView.measuredHeight) * FloatViewManger.proportionY
        }
        dragView.layout(
            lastX.toInt(),
            lastY.toInt(),
            lastX.toInt() + dragView.measuredWidth,
            lastY.toInt() + dragView.measuredHeight
        )
    }
}

注意:关联用例弹层需求添加到覆盖率SDK中,直接调用前端覆盖率生成逻辑进行控制即可。

3,deeplink响应处理

我们需要在覆盖率SDK中添加对deeplink请求的处理,解析出deeplink传递过来的用例信息,如下所示:
import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;

import com.yiqixie.androidcodecoverage.getcodecoverage.JacocoGenerateCoverage;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class DeepLinkForManualTest {

    /**
     * 通过DeepLink关联手工用例
     * @param intent
     */
    public  void bindManualTestCases(Intent intent){
        Log.d("DeepLink","开始启动DeepLink,关联手工用例");
        if(intent==null){
            Log.d("DeepLink","intent==null");
        }
        Uri data = intent.getData();
        if (data != null) {
            String action = data.getQueryParameter("action");
            String caseid = data.getQueryParameter("caseid");
            if(action.indexOf("setUp")>-1){
                //开始关联手工测试用例弹窗
                LinkManualTestCases.INSTANCE.show(caseid);
                Log.d("DeepLink","开始收集覆盖率数据,用例ID:"+caseid);

            }else {
                //结束关联手工用例,Toast提醒
                LinkManualTestCases.INSTANCE.showtoast(caseid);
                Log.d("DeepLink","弹出结束用例提醒!");
            }
        }else{
            Log.d("DeepLink","Data为空!!!");
        }

    }
}

4.2.3 添加关联用例功能

当关联用例功能开发完成后,被测应用要关联用例时,需要按如下步骤进行集成。

1,集成覆盖率SDK

将覆盖率SDK添加到被测应用中,注意对应用和模块的引用,这是采集覆盖率数据必须的。

2,添加deeplink响应

在App AndroidManifest.xml文件中添加如下代码:
<!--添加 deeplink测试 url跳转格式为:open://app.test.com/game-->
   <intent-filter>
      <action android:name="android.intent.action.VIEW" />
         <category android:name="android.intent.category.BROWSABLE" />
           <category android:name="android.intent.category.DEFAULT" />
           <data
               android:scheme="recase"
               android:host="example.test"
               android:pathPrefix="/autotest"
             />
 </intent-filter>

打包后,就可以通过如下调用传递参数:
adb shell am start -W -a android.intent.action.VIEW -d "recase://example.test/autotest?action=setUp\&caseid=LoginTest"

3,覆盖率调用

    在App最底层的Activity中添加上添加弹层的操作,保证应用打开的时候完成弹层的初始化工作。在合适的位置添加上对deeplink的调用,以便应用能正确处理deeplink请求。具体的位置因应用不同,要修改的文件位置也不一样,所以此处无法给出具体的位置和示例。
配置完成后,打包即可,打出的包就能响应关联用例操作。

4.2.4 用例管理平台关联用例

在大多数公司都会有一个用例管理平台,无论你使用的是开源的,商业化的还是自研的,用来管理每个需求对应的手工用例。而精准测试的追溯关系,就是要建立手工用例与代码的关联关系。所以如何把用例信息传给覆盖率SDK,创建关联关系,这个是非常重要。

1,手工输入用例ID

      在关联用例弹层中,如果把用例信息输入框变成可编辑的,就可以直接输入用例ID,然后关联用例。如果采用此方案,就不需要deeplink相关功能,但是存在着很多用例平台的ID非常长,或是比较复杂,如果用户输入错误,就无法做后面用例解析,从而实现用例与代码的关联关系。

2,deeplink传参

     用例管理平台可以添加一个功能,通过向手机传递deeplink请求,将用例ID传递给应用。这有两个方法,如果手机能连接电话,可以直接通过adb请求deeplink;如果不想连接手机,可以通过atx-agent启动的服务,请求Agent相关接口发送deeplink。

3,接口传用例信息

      如果用例管理平台无法与手机进行通信,存在内外网不通的问题,就需要优化一下关联用例逻辑。借助于精准测试平台与用例平台互动,获取用例信息,关联用例弹层调用精准测试平台的接口,获取用例ID,同时进行用例ID的切换。注意要以手机设备号区分关联用例操作的人员,以便兼容多个人员同时关联用例。
    上面的方案存在着优化用例管理平台,安装atx-agent以及添加精准测试平台用例相关功能,请结合自己的业务选择合适的方案。

4.2.5 覆盖率解析

现在我们可以得到覆盖率数据文件和用例ID信息,那么如何解析出覆盖率数据对应的代码信息呢?

1,解析覆盖率ec文件

通过解析覆盖率ec文件,解析出覆盖率文件中覆盖的文件以及对应的行号。具体方法是,使用jacoco开源的项目,编写解析ec文件的工具,核心代码如下:
import com.google.gson.Gson
import org.jacoco.core.analysis.Analyzer
import org.jacoco.core.analysis.CoverageBuilder
import org.jacoco.core.analysis.ICounter
import org.jacoco.core.tools.ExecFileLoader
import java.io.File

/*******************************************************************************
 *  Read jacoco exec file, java class file, and source file to produce coverage lines.
 */
data class CoverageInfo(
    val filename: String,
    var covered: Set<Int>,
    var nocovered: Set<Int>
)

class JacocoParserOperation {

    val Covered = setOf(ICounter.FULLY_COVERED, ICounter.PARTLY_COVERED)
    val NoCovered = setOf(ICounter.NOT_COVERED)

    fun readJacocoECFileContent(ecFileNames:Array<File>,classesDir:File,sourceDir: Array<File>,output: File){
        val ecFileLoader = ExecFileLoader()
        for (file in ecFileNames) {
            ecFileLoader.load(file)
        }

        val coverageBuild = CoverageBuilder()
        val analyzer = Analyzer(ecFileLoader.executionDataStore, coverageBuild)

        analyzer.analyzeAll(classesDir)

        val sourceFileGroup = sourceDir.map {
            it.walk().filter { file -> file.isFile }.toSet()
        }.fold(setOf<File>()) { s, e -> s + e}.groupBy { it.name }

        val bundle = coverageBuild.getBundle("")
        val coverageMap = mutableMapOf<String, CoverageInfo>()
        bundle.packages.map { pkg ->
            for (c in pkg.classes) {
                val sourceFile =
                    sourceFileGroup[c.sourceFileName]?.findLast { f -> f.path.indexOf(c.packageName) > 0 } ?: continue

                var coverageInfo = coverageMap[sourceFile.toString()]
                if (coverageInfo == null) {
                    coverageInfo = CoverageInfo(sourceFile.toString(), setOf(), setOf())
                }
                for (i in c.firstLine..c.lastLine) {
                    if (c.getLine(i).status in Covered) {
                        coverageInfo.covered += i
                    } else if (c.getLine(i).status in NoCovered) {
                        coverageInfo.nocovered += i
                    }
                }
                coverageMap[sourceFile.toString()] = coverageInfo
            }
        }

        val gson = Gson()
        val content = gson.toJson(coverageMap)
        output.writeText(content)

    }
}

2,通过文件和行号查询函数

通过文件与覆盖的行号列表,调用调用链路CCG服务,通过javaparser-ast.jar解析出相应的行对应该的函数列表;最后得到如下所示的数据:
{"类文件1":["fun1","fun2"...],"类文件2":["fun1","fun2"...],"类文件3":["fun1","fun2"...]}

获取到类覆盖的文件和函数列表后,就可以写到追溯关系表中,创建用例和代码的追溯关系了。后续就要以根据追溯关系,根据需求修改代码的影响面,推荐出最小用例集,达到最大的覆盖范围。

网站公告

今日签到

点亮在社区的每一天
去签到