Skip to content

KotSuite 问题反馈跟踪 6.9

1 会议讨论问题

  1. 项目整体进度
  2. 单测用例基于实际的项目进行,暂时以原生代码相册,时钟,日历三个项目做实验,后续的测试报告也基于这三个项目为基础:
    1. 时钟:git clone https://android.googlesource.com/platform/packages/apps/AlarmClock master分支
    2. 计算器:git clone https://android.googlesource.com/platform/packages/apps/ExactCalculator
    3. 日历:git clone https://android.googlesource.com/platform/packages/apps/Calendar
    4. 相册:git clone https://android.googlesource.com/platform/packages/apps/Gallery2
  3. 单元测试用例一些基础技术指标沟通:
    1. 保底:代码覆盖率100%;正常:判断覆盖率100%;冗余率(重叠的、无意义的测试用例):<10%;
      1. 代码覆盖率和判断覆盖率无法达到 100%
      2. 冗余的定义是什么?(覆盖不同 or 测试职责不同)
      3. 不会强制要求100%
      4. 冗余:覆盖路径不同,标准模糊?
      5. 冗余指标待定
    2. 可独立执行
      1. 可以实现
    3. 可重复执行
      1. 可以实现
      2. 要求多次重复执行的执行结果相同:可能会出现 flaky test,不能完全保证,概率较小
    4. 支持测试框架:JUnit等框架;
      1. 可以实现
      2. 版本:junit 4.11/4.12
    5. 支持和正确使用常用的测试替身技术(包括自动生成测试替身):Stub、Mock、Spy、Fake等;
      1. 成本较高,需要进一步调研
      2. 演示:不使用测试替身技术是如何生成的,lazy init
      3. 如果不影响覆盖率,也可以不使用
    6. 具有较好的可读性
      1. 标准是什么?(命名:fun `should return empty when convert with isAppSupportHEIF is true`() 无法做到)
      2. 算法无法理解程序的意图
      3. 只要求命名规范,驼峰命名
    7. 修改代码率 < 10%(通过修改才能正确运行)
      1. 生成得到的测试用例都能编译运行
      2. 正确运行 == 能编译运行 ?
      3. 正确运行:能编译运行
      4. 每次算法独立执行,不会互相影响
      5. 用户新增一些方法后,如何再次运行算法 => 粒度缩小到方法级别? 成本较高,后续再考虑
    8. 断言准确性 > 95%
      1. 生成的断言只能用于回归测试,无法发现当前代码中的 bug
      2. 准确性:回归测试中发现 bug 的准确性
      3. 会出现误报问题,漏报问题
      4. 没有办法达到,可以以其他工具作为标准,e.g., evosuite 50%
      5. 提供:论文
    9. 单一职责的测试用例占比 > 90%(是否每个用例测试单一的方面)
      1. 单一职责指什么?
      2. 算法无法理解程序的意图
      3. 无法做到,不讨论
    10. 生成测试用例的方法(边界、等价类、因果图)
      1. 成本较高,需要进一步调研
      2. 说明:实现难度、成本、价值如何(发现多少问题,冗余度如何),为什么落地很难,目前有什么工具

2 回复

2.1 项目整体进度

算法

  • 遗传算法
    • 变异
    • 重组
    • 适应度计算
  • 反编译输出 java 文件
  • 输出为符合 JUnit 规范的测试用例
  • 测试用例重用
  • 断言生成
  • AndroidTest 生成
  • 测试用例去重

遗留工作:

  1. 遗传算法的重组部分(6.5);
  2. 断言生成(6.12);
  3. 测试用例约简去重(6.19);
  4. AndroidTest 生成(7.3);
  5. 测试用例重用(7.17)。

插件

功能已全部完成,反馈内容已全部更新。

2.2 基础技术指标沟通

测试生成工具覆盖率(行覆盖率):

image-20230609130659853

人工测试用例命名示例:

class PathsToClipDataConvertTest {
    @Test
    fun `should return null value when convert with null`() {}

    @Test
    fun `should return empty when convert with isAppSupportHEIF is true`() {}

    @Test
    fun `should return empty when convert with isPrefHeifConvertEnable is false`() {}

    @Test
    fun `should return empty when convert with isHeifConvertEnable is false`() {}

    @Test
    fun `should return empty when convert with checkEnableTransform is false`() {}

    @Test
    fun `should return empty when convert with needConvertHeif and needConvertHlg are all false`() {}

    @Test
    fun `should return right image uris when convert with list`() {}

    @Test
    fun `should return null value when convert with null`() {}

    @Test
    fun `should return right image uri when convert with image path`() {}

    @Test
    fun `should return right video uri when convert with video path`() {}

    @Test
    fun `should return null value when convert with null`() {}

    @Test
    fun `should return right image uris when convert with list`() {}
}

EvoSuite 生成测试用例命名示例:

public class AbstractCategoryItemRenderer_ESTest {

    @Test(timeout = 4000)
    public void test34()  throws Throwable  {
        CategoryStepRenderer categoryStepRenderer0 = new CategoryStepRenderer();
        DefaultPieDataset defaultPieDataset0 = new DefaultPieDataset();
        PiePlot3D piePlot3D0 = new PiePlot3D(defaultPieDataset0);
        JFreeChart jFreeChart0 = new JFreeChart("Null 'stroke' argument.", categoryStepRenderer0.DEFAULT_VALUE_LABEL_FONT, piePlot3D0, false);
        BufferedImage bufferedImage0 = jFreeChart0.createBufferedImage(5, 5);
        Graphics2D graphics2D0 = bufferedImage0.createGraphics();
        Rectangle rectangle0 = new Rectangle((-2554), (-286), (-2554), 8);
        NumberAxis numberAxis0 = new NumberAxis("Null 'stroke' argument.");
        StandardEntityCollection standardEntityCollection0 = new StandardEntityCollection();
        ChartRenderingInfo chartRenderingInfo0 = new ChartRenderingInfo(standardEntityCollection0);
        PlotRenderingInfo plotRenderingInfo0 = chartRenderingInfo0.getPlotInfo();
        CombinedRangeCategoryPlot combinedRangeCategoryPlot0 = new CombinedRangeCategoryPlot(numberAxis0);
        BoxAndWhiskerRenderer boxAndWhiskerRenderer0 = new BoxAndWhiskerRenderer();
        // Undeclared exception!
        try { 
        boxAndWhiskerRenderer0.initialise(graphics2D0, rectangle0, combinedRangeCategoryPlot0, (CategoryDataset) null, plotRenderingInfo0);
        fail("Expecting exception: NullPointerException");

        } catch(NullPointerException e) {
            //
            // no message in exception (getMessage() returned null)
            //
            verifyException("org.jfree.chart.renderer.category.AbstractCategoryItemRenderer", e);
        }
    }

    @Test(timeout = 4000)
    public void test35()  throws Throwable  {
        BoxAndWhiskerRenderer boxAndWhiskerRenderer0 = new BoxAndWhiskerRenderer();
        CombinedRangeCategoryPlot combinedRangeCategoryPlot0 = new CombinedRangeCategoryPlot();
        Connection connection0 = mock(Connection.class, new ViolatedAssumptionAnswer());
        JDBCCategoryDataset jDBCCategoryDataset0 = new JDBCCategoryDataset(connection0);
        StandardEntityCollection standardEntityCollection0 = new StandardEntityCollection();
        ChartRenderingInfo chartRenderingInfo0 = new ChartRenderingInfo(standardEntityCollection0);
        PlotRenderingInfo plotRenderingInfo0 = new PlotRenderingInfo(chartRenderingInfo0);
        // Undeclared exception!
        try { 
        boxAndWhiskerRenderer0.initialise((Graphics2D) null, (Rectangle2D) null, combinedRangeCategoryPlot0, jDBCCategoryDataset0, plotRenderingInfo0);
        fail("Expecting exception: IllegalArgumentException");

        } catch(IllegalArgumentException e) {
            //
            // Negative 'index'.
            //
            verifyException("org.jfree.chart.plot.CategoryPlot", e);
        }
    }

}

断言:

  • A fault in the current version of the program can only be detected if the developer verifies the correctness of the synthesized assertion and identifies a problem.

  • Similarly, a test case might fail at a later point, indicating a regression failure, when in fact having too many assertions might simply overspecify the test case, leading to false alarms.

3 沟通结果

6.16 邮件

根据上周五会议沟通,重新梳理单测指标,分成以下四类。

  1. 请对立项内和不在立项但是作为单元测试基础能力需要关注的指标的达成风险做一个确认。
  2. 对于后期实现或者实现不了的,请帮忙做一些学术或者行业现状的简单分析,我们需要做一些ROI识别。
  3. 对于单测自动生成工具之前杨枫分享的一个论文片段,有介绍一下对比指标,希望能对这些指标做一些指导说明并针对我们的工具一起做技术上的对比。

image-20230616201021034

另外还有两个遗留事项请杨枫帮忙提供一下:

  1. 没有测试替身,如何实现代码中的类参单测用例生成,给出实际代码案例介绍。
  2. 上次会议上看到的论文请帮忙做分享

4. 邮件回复

4.1 指标达成风险确认

如下表所示,以目前广泛应用的 EvoSuite 工具进行参考,对下列指标的达成风险进行了完善。

image-20230627220518609

4.2 后期实现/实现不了的指标的现状分析

a. 生成测试用例的方法(边界、等价类、因果图)

算法综述

附件“测试用例生成综述.pdf”是目前普通 Java 生成测试用例算法和工具的综述。

目前被广泛应用的工具有:

  • EvoSuite:使用基于搜索的测试用例生成算法(即遗传算法)
  • Randoop:使用基于随机算法的测试用例生成算法
  • AgitarOne:商业测试用例生成工具
基于边界条件的测试生成
  • 对应算法:基于符号执行算法的测试生成
  • 描述:在符号执行算法中会收集到测试输入的边界条件,并使用约束求解器得到对应的测试输入
  • 限制:符号执行算法存在路径爆炸和环境依赖的问题,同时基于符号执行算法的测试生成耗时很长
    • 路径爆炸:由于 Android 项目的规模大、复杂度高,约束求解器难以求解得到边界条件
    • 环境依赖:Android 项目依赖于 Android framewok 和第三方库,使其无法求解
    • 时间成本:符号执行需要符号化地执行每一条路径,并收集约束再求解,这个过程需要消耗很大的时间成本
基于等价类的测试生成
  • 对应算法:基于组合测试的测试生成
  • 描述:在组合测试中,通过选择适当的参数值组合,以覆盖可能的交互效应和错误模式,以发现隐藏的错误和故障。组合测试方法可以显著减少测试用例的数量,提高测试效率,同时保持对系统的全面覆盖。
  • 限制:组合测试一般是通过测试多个输入值的组合来发现可能存在的缺陷,并不适用于 Android 单元测试用例生成,它的应用场景包括以下几个方面:
    • 参数配置测试:软件通常具有各种参数和配置选项,组合测试可用于测试不同参数组合下的行为和性能。例如,一个网络路由器可能具有多个配置选项,通过组合测试可以验证各种配置组合是否正常工作。
    • 操作系统和平台兼容性测试:在不同的操作系统、硬件平台或软件环境下,软件可能会产生不同的行为。通过组合测试,可以覆盖不同的操作系统版本、硬件配置和软件组合,以验证软件在各种环境下的兼容性。
    • 接口测试:当软件系统与其他系统或组件进行交互时,需要进行接口测试。组合测试可用于测试各种接口参数组合的行为。例如,一个电子商务系统的支付接口可能有多个参数,通过组合测试可以验证各种支付方式和金额的组合是否正常处理。
    • 多语言和本地化测试:软件的多语言支持和本地化是一个重要的测试方面。组合测试可用于测试不同语言环境下的软件行为和用户界面的正确性。
    • 功能测试:在功能测试中,组合测试可以用于测试不同功能的交叉影响。例如,一个电子邮件客户端可能具有多个功能,通过组合测试可以测试不同功能组合下的正确性和一致性。
    • 安全性测试:在安全性测试中,组合测试可以用于测试各种安全漏洞的组合。通过模拟不同的攻击场景和参数组合,可以发现可能的安全漏洞和风险。
基于因果图的测试生成
  • 对应算法:基于模型的测试生成
  • 描述:通过建立被测软件的因果关系的抽象描述模型,从模型中派生出测试用例,以覆盖不同因果关系的组合。其一般应用于 UI 测试,通过构建 Android UI 的抽象模型,在模型的基础上构建 UI 测试用例。
  • 限制:单元测试用例针对的是独立的被测单元,并不适用于基于模型的测试生成。

b. 支持和正确使用常用的测试替身技术

现状:

在普通 Java 项目测试用例生成中,已经有测试生成工具(如 EvoSuite)在生成的测试用例中使用了测试替身技术,但目前还没有工具在 Android 项目上使用测试替身技术。

参考论文:

Automated unit test generation for classes with environment dependencies

应用场景:

Mock 待测方法中的文件系统交互、数据库交互和网络交互等。

在 Android 项目的测试生成中使用测试替身技术的难点:

  • 需要为 Android Platform 封装一些 mock 对象,实现成本较大。
  • Android 项目的依赖更加复杂,不能简单考虑待测函数参数的 mock,还要考虑 Android 平台中对象的 mock,需要考虑新的算法。

c. 断言准确性

断言准确性的定义:回归测试中发现的真实 bug 的数量 / 实际 bug 的数量。

回归测试中的误报:回归测试中测试用例执行失败,但并不是由于存在 bug 引起的,而是由于断言约束过强引起的。

现状:EvoSuite 在实际 Java 项目中的平均断言准确性为 32.3%。

注:一般来说,为了使得断言准确性更高,需要使用约束更强的断言,但这会引起误报率升高,因此需要在断言准确性和误报率中做一个平衡。

d. 单一职责的测试用例占比

算法无法理解程序的真实意图,因此无法做到每个测试用例都是单一职责的。

现有的测试生成工具包含了测试用例约简算法,可以使得不同的测试用例的覆盖路径不同。

4.3 现有测试生成工具的指标对比与分析

image-20230624203303258

表 I 是对三个测试用例生成工具(AgitarOne、EvoSuite 和 Randoop)和人工编写测试用例在 5 个 Java 项目(Chart、Closure、Lang、Math 和 Time)上多次生成测试用例的效果对比。其中 flaky test 指多次执行结果不稳定的测试用例。

  • Compilable 表示可以通过编译的测试用例占比;
  • Tests 表示平均生成的测试用例数量;
  • Flaky 表示生成的测试用例中 falky test 所占的比例;
  • False Pos. 表示在回归测试中误报的测试用例的比例;
  • Coverage 表示测试用例在包含错误的类上的覆盖率;
  • Max Bugs 表示测试用例发现的 bug 的最大数量(排除误报的 bug);
  • Avg. Bugs 表示测试用例发现的 bug 的平均数量(排除误报的 bug);
  • Assertion 表示由于断言错误发现的 bug 的数量;
  • Exception 表示由于测试用例抛出异常发现的 bug 的数量;
  • Timeout 表示由于测试用例执行超时发现的 bug 数量。

结论:

  1. 生成的测试用例中可能存在少部分的 flaky test,即测试用例多次执行的结果不稳定;
  2. 生成的测试用例的覆盖率为 34.5% - 86.7% 之间,平均覆盖率为 70.5%(以 EvoSuite 为例),说明对于不同的待测项目能达到的覆盖率差异较大。

image-20230627225906426

同时,根据表 II,EvoSuite 的平均断言准确性为 32.2%。

4.4 无测试替身的类参单测用例生成

当待测函数的参数为复杂类型时,可以不使用测试替身,而是使用实际构造对象。

下面是一个 Android 项目代码片段,其中的 doSomething() 为待测方法,MyActivity 类中有一个 MyService 类型的成员变量。

interface MyService {
    fun doSomething(): String
}

class MyActivity {
    lateinit var myService: MyService

    fun doSomething(): String {
        // Perform some logic using myService
        return myService.doSomething()
    }
}

下面是使用 MockK 框架的测试用例示例,其中使用 MockK 设置了 doSomething() 方法的行为(Line 26)。

import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class MyActivityMockkTest {

    private lateinit var myService: MyService
    private lateinit var myActivity: MyActivity

    @Before
    fun setup() {
        myService = mockk()
        myActivity = MyActivity()
        myActivity.myService = myService
    }

    @Test
    fun testDoSomething() {
        // Arrange
        val expectedResult = "Result"
        every { myService.doSomething() } returns expectedResult

        // Act
        val result = myActivity.doSomething()

        // Assert
        assertEquals(expectedResult, result)
    }
}

下面是不使用 Mock 框架的测试用例示例。

  1. setUp() 方法中,我们首先初始化 myActivity 对象;
  2. 当初始化 myService 成员变量时,首先寻找当前是否存在 MyService 类型的类,
    1. 若存在,则使用该类的构造函数构造一个对象赋给myService
    2. 若不存在,则 mock 一个 MyService 类型的类 MyServiceMock,并重写其中的 doSomething() 方法,然后构造一个 MyServiceMock 的对象赋给 myService(Line 30)。
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class MyActivityTest {

    private lateinit var myActivity: MyActivity
    private lateinit var myService: MyServiceMock

    @Before
    fun setup() {
        myService = MyServiceMock()
        myActivity = MyActivity()
        myActivity.myService = myService
    }

    @Test
    fun testDoSomething() {
        // Arrange
        val expectedResult = "Result"

        // Act
        val result = myActivity.doSomething()

        // Assert
        assertEquals(expectedResult, result)
    }

    // Mock implementation of MyService
    class MyServiceMock : MyService {
        override fun doSomething(): String {
            return "Result"
        }
    }
}

4.5 论文分享

附件“Shamshiri et al_2015_Do Automatically Generated Unit Tests Find Real Faults.pdf”是上次会议中提到的论文。

论文链接:Do Automatically Generated Unit Tests Find Real Faults? An Empirical Study of Effectiveness and Challenges (T)