研发效能 适用于 android/jvm 的单测生成器 randunit

williamfzc · 2021年04月08日 · 最后由 乱舞 回复于 2021年09月02日 · 3288 次阅读
本帖已被设为精华帖!

背景

在持续交付越来越流行的今天,单测作为保障 CI 质量的重要一环也开始在国内被重视起来。

不过在单测上大家的态度还是比较矛盾的,担心的事情主要有两个:

  • 从 0 到 1 的第一步不知道从何下手
  • 万一花大力气做了之后发现没什么用怎么办

但是缺少这一环,在整个 devops 流程中很多编译时严重问题会被延缓到运行时暴露,这对于项目效率阻碍也不小。。

于是就有了灵感来源:

  • 自动生成一系列冒烟级别单测用例,并能发现严重问题
  • 接入成本低,能无痛与现有流程结合
  • 能对 android 生效(我司重移动端

做了什么

RandUnit 取义自 Random UnitTest,他会:

  • 根据提供的包名或入口类,搜索所有相关的待测试类与方法
  • 根据搜索结果,为每个方法生成一系列 statements 用于测试
  • 像常规单测流程一般,在 junit 上运行这些 statements,得到测试结果

而这一切只需要一次简单的复制粘贴:

import com.williamfzc.randunit.env.NormalTestEnv
import com.williamfzc.randunit.models.StatementModel
import com.williamfzc.randunit.scanner.ScannerConfig
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class MinExampleTest(private val statementModel: StatementModel) {
    companion object {
        private val testEnv = NormalTestEnv()
        private const val packageName = "com.your.package"
        private val cases by lazy {
            val scannerConfig = ScannerConfig()
            scannerConfig.includeFilter.add(packageName)

            RandUnit.collectStatementsWithPackage(packageName, scannerConfig)
        }

        @JvmStatic
        @user3ters(name = "{0}")
        fun data(): Collection<StatementModel> {
            return cases
        }
    }

    @Test
    fun runStatements() {
        testEnv.runStatementInSandbox(statementModel)
    }
}

由于它是合法的 junit 用例,所以你可以在 ide 里直接运行它。直接 run with coverage 的话:

可以看到,它帮你生成了 500+ 条用例并执行了,并且达到了不错的覆盖率。

解决了啥

再回头看看是不是解决了上面的三个问题:

  • 自动生成:目前的策略是在每次编译后自动搜索生成一系列 junit 用例并执行,有代码变更也无需改用例
  • 接入成本低:一次粘贴即可接入。除此之外,因为他是合法 junit 用例,那自然也可以像正常使用 junit 用例一样用它,例如在 CI 环境里
  • 对 android 生效:得益于 robolectric 的支持, randunit 能够很好地被应用到 android 项目中,而不需要真机。不过上面的代码需要有微调,感兴趣可以看看项目主页。

像这种问题:

override fun getCastOptions(context: Context?): CastOptions? {

    // oh, you import a non-existed class here!
    // it should cause a ClassNotFoundException
    Class.forName("import unknown class here!")

    ...
}

该类型问题在编译期并不能被发现,问题越晚暴露 == 修复成本越高。使用之后就不会被遗漏到集成之后啦:

WARNING: error happened inside sandbox: java.lang.reflect.InvocationTargetException

java.lang.ClassNotFoundException: import unknown class here!

    at org.robolectric.internal.bytecode.SandboxClassLoader.getByteCode(SandboxClassLoader.java:158)

什么时候推荐使用 randunit

  • 在之前很长一段时间的观察里,无论项目大小,依旧有大量开发中的项目处在没有任何单元测试的状态中。随着 devops 的流行,持续化的自动化测试几乎已经成为整个敏捷流程中最为关键的一环。
  • 而在这种情况下其实很多业务认知到了这一阶段的重要性,但不知道从何下手
  • 这个项目的出发点就是,用尽可能少的成本将单元测试跑起来,至少将这里的空缺填补起来

推荐使用的情况

你的项目目前还处于裸奔的状态,或单元测试做得不好的状态

不推荐使用的情况

你想要一个彻底发现问题并能完整覆盖整个项目的工具

原型与演进方向

有一些朋友肯定会拿 evosuite 跟 randoop 出来杠我,而事实上这个工程的灵感就源自他们。感兴趣可以看:原型与演进方向

链接

共收到 36 条回复 时间 点赞

先占个前排留赞

对 kotlin 版本啥的有要求么?拿个开源项目加上文档里给的测试代码试了下,没能跑起来

对应项目地址:https://github.com/chenhengjie123/AgileTC/tree/try-randunit ,分支是 try-randunit 。

运行方式是直接在 idea 中点击测试用例前面的执行按钮触发执行。

错误提示:

java.lang.NoClassDefFoundError: org/jeasy/random/EasyRandomParameters

    at com.williamfzc.randunit.mock.EasyRandomMocker.genDefaultEasyRandomParameters(EasyRandomMocker.kt:31)
    at com.williamfzc.randunit.mock.EasyRandomMocker.<init>(EasyRandomMocker.kt:23)
    at com.williamfzc.randunit.models.MockModel.<init>(MockModel.kt:31)
    at com.williamfzc.randunit.env.AbstractTestEnv.<init>(AbstractTestEnv.kt:32)
    at com.williamfzc.randunit.env.NormalTestEnv.<init>(NormalTestEnv.kt:21)
    at com.williamfzc.randunit.env.NormalTestEnv.<init>(NormalTestEnv.kt:20)
    at com.xiaoju.framework.SmokeTest.<clinit>(SmokeTest.kt:16)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.runners.Parameterized.allParameters(Parameterized.java:280)
    at org.junit.runners.Parameterized.<init>(Parameterized.java:248)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104)
    at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86)
    at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
    at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26)
    at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
    at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:33)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:49)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.ClassNotFoundException: org.jeasy.random.EasyRandomParameters
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    ... 30 more
陈恒捷 回复

是少了 mock 库的依赖,我之前一直在 gradle 上跑没试过 maven,拿你的工程试下~

陈恒捷 回复

是可以跑的,你应该是用 jar 包直接引用了?jar 引用的情况下他的依赖需要在 pom.xml 里挨个指定;
直接引用 jitpack 的可能好一些,配个仓库就好了:

<repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
</repositories>

然后就行了:

覆盖率不高可以参考:https://github.com/williamfzc/randunit#faq

williamfzc 回复

好,加了仓库后好了。感谢

我特意再限制了只跑 util 包里的,避免依赖其他东西跑不出来。看跑出来结果还有些效果,部分空指针没做好处理的有发现了。但也有一些无效的失败,比如文件名是随机的,导致文件不存在,文件 io 操作报异常导致失败;或者 json 格式错误,导致 Json 解析失败。

从目前情况看,会有一定的误报,测试结果还是得人工二次确认。找空指针或者数组越界有一定效果,但业务逻辑就基本都不怎么能校验到了。

PS:建议给些用 java + maven 的例子? kt + gradle 在 android 比较流行,但非 android 相对比较少。

这个适用 SDK 项目嘛?

陈恒捷 将本帖设为了精华贴 04月08日 15:09
陈恒捷 回复

对的,误报确实是个问题,现在推荐的方式是通过配置 rule。配一次之后后面就可以比较稳定跑了:

https://github.com/williamfzc/randunit#%E5%A6%82%E4%BD%95%E5%BF%BD%E7%95%A5%E6%97%A0%E6%95%88%E9%94%99%E8%AF%AF

java+gradle 现在是有的(https://github.com/williamfzc/randunit/blob/main/randunit-demo),后面给出 java+maven 的例子~

williamfzc 回复

额,你这个 java 例子,测试用例还是只有 kotlin 的。我指的是测试用例就是 java 的。

虽然 kotlin 只要有 idea 都很便捷地跑起来,但还得临时学一些语法才知道怎么改里面的内容。如果有直接 java 版会好很多。

陈恒捷 回复

明白了~ 现在是有的,只是放的位置确实比较零散 😂

java 用例:https://github.com/williamfzc/randunit/blob/main/randunit/src/test/java/com/williamfzc/randunit/SelfSmokeWithJUnit4InJavaTest.java

后续补上

期待 java+maven 的例子

mark 一下,抽空研究下,然后再去公司推广

感谢楼主,收录在社区公众号文章中!

maven 没有 randunit 这个包。 请问哪里可以下到
Could not find artifact com.github.williamfzc.randunit:randunit:pom:0.1.1

chen 回复

https://github.com/williamfzc/randunit#installation

这里有写需要加上 jitpack 仓库的

williamfzc 回复

感谢,可以了

您好,我用上面留言中的开源项目试了一下,https://github.com/chenhengjie123/AgileTC/tree/try-randunit ,执行时报错,,我有加上 jitpack 仓库,请问还有可能是什么原因报错了呢?

我更新了下这个分支代码,把加仓库的相关修改加上了。你看下和你姿势是不是一样?

我已经调整到可以直接 mvn test 就跑起单测了。

在 Android 工程中引入 randunit 依赖后编译报错:

陈恒捷 回复

十分感谢,可以了

@williamfzc 请教下,如果一个成型的安卓项目,如何从 0 开始,从基础的配置开始完成这个工具的使用?希望有一个小白教程,谢谢!

ftvbftvbq 回复

https://github.com/williamfzc/uamp/commit/af36299bd4f2ce10eba39ec44914d56776a378f9

可以参考下这次提交,这个囊括了 基于 google 写的 app,接入了这个工具 所需要修改的代码

williamfzc 回复

谢谢,这个问题已经解决了,又遇到一个新问题,在项目中实践的时候发现会卡在某一个 runStatements 上,一直不结束

duncan_89 回复

配一下 junit 原生的 timeout 就好了

@williamfzc 你搞个钉钉群吧?试用下来各种小问题不断,头大,一起来看下解决问题,促进项目持续发展吧

@williamfzc
Java+maven 的项目就按你说的这种方式来吗?

maven 方式接入:https://github.com/williamfzc/randunit#maven
java 例子:https://github.com/williamfzc/randunit/blob/main/randunit/src/test/java/com/williamfzc/randunit/SelfSmokeWithJUnit4InJavaTest.java

就是在被测试项目里加入这 2 个依赖:


com.github.williamfzc.randunit
randunit
0.1.1


jitpack.io
https://jitpack.io

然后再把这个测试类 SelfSmokeWithJUnit4InJavaTest.java 一模一样拷进去就好了?

但是我试了 3 个项目, 每个项目跑出来都是 183 个 case,

这 3 个是不同的项目, 但跑出来的 case 都一样, 也没测出问题, 是我接的不对吗,理解错了?

sf494579359 回复

接入没错,里面扫描的包名要改成你自己的包名呀,不然默认一直扫描的是 randunit 本体。。

williamfzc 回复


就这个地方改下包名吗?跑了还是不行,0 个 case,再麻烦帮我看下?

sf494579359 回复

换成你自己的类

williamfzc 回复

那跑一个项目的 case, 只能一个类一个类的跑, 不能所有类的 case 一起跑了?

sf494579359 回复

那是一个 set.. 要多少个加进去就是了
包名搜索其他的例子里有

问下生成的 case 在什么地方,只看见跑了很多 case,没有看见 case 具体是什么样的

占楼,留后学习


用内部项目尝试了一下,行覆盖率有点偏低,大致在 30% 左右。按照谷歌的行覆盖率标准,仅用这款工具,目前还达不到 acceptable 标准。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册