如何优雅地单元测试 Kotlin/Java 中的 private 方法?
翻译自 https://medium.com/mindorks/how-to-unit-test-private-methods-in-java-and-kotlin-d3cae49dccd
❓如何单元测试 Kotlin/Java 中的 private 方法❓
首先,开发者应该测试代码里的 private 私有方法吗?
直接信任这些私有方法,测试到调用它们的公开方法感觉就够了吧。
对于这个争论,每个开发者都会有自己的观点。
但回到开头的问题本身,到底有没有一种合适的途径来实现私有方法的单元测试?
截止到目前,在面对单元测试私有方法的问题时,一般有如下几种选择:
-
不去测试私有方法 😜*(选择信任,直接躺平)*
-
将目标方法临时改成 public 公开访问权限 😒(可我不愿意这样做,这不符合代码规范。作为一名开发者,我要遵循最佳实践)
-
使用嵌套的测试类 😒*(将测试代码和生产代码混到一起不太好吧,我再强调一遍:我是很优秀的开发者,要遵循最佳实践)*
-
使用 Java 反射机制 😃*(听起来还行,可以试试这个方案)*
大家都知道通过 Java 反射机制可以访问到其他类中的私有属性和方法,而且写起来也不麻烦,在单元测试里采用该机制应该也很容易上手。
注意:
只有将代码作为独立的 Java 程序运行时,这个方案才适用,就像单元测试、常规的 Java 应用程序。但如果在 Java Applet 上执行反射,则需要对 SecurityManager
做些干预。由于这不是高频场景,本文不对其作额外阐述。
Java 8 中添加了对反射方法参数的支持,使得开发者可以在运行时获得参数名称。
访问私有属性
Class
类提供的 getField(String name)
和 getFields()
只能返回公开访问权限的属性,访问私有权限的属性则需要调用 getDeclaredField(String name)
或 getDeclaredFields()
。
下面是一个简单的代码示例:一个拥有私有属性的类以及如何通过 Java 反射来访问这个属性。
public class PrivateObject {private String privateString = null;public PrivateObject(String privateString) {this.privateString = privateString;}
}PrivateObject privateObject = new PrivateObject("The Private Value");
Field privateStringField = PrivateObject.class.getDeclaredField("privateString");privateStringField.setAccessible(true);String fieldValue = (String) privateStringField.get(privateObject);System.out.println("fieldValue = " + fieldValue);
上述代码将打印出如下结果:内容来自于 PrivateObject
实例的私有属性 privateString
的值。
fieldValue = The Private Value
需要留意的是,getDeclaredField("privateString")
能返回私有属性没错,但其范围仅限 class 本身,不包含其父类中定义的属性。
还有一点是需要调用 Field.setAcessible(true)
,目的在于关闭反射里该 Field 的访问检查。
这样的话,如果访问的属性是私有的、受保护的或者包可见的,即使调用者不满足访问条件,仍然可以在反射里获取到该属性。当然,非反射的正常代码里依然无法获取到该属性,不受影响。
访问私有方法
和访问私有属性一样,访问私有方法需要调用 Class
类提供的 getDeclaredMethod(String name, Class[] parameterTypes)
或 Class.getDeclaredMethods()
。
同样的,我们展示一段代码示例:定义了私有方法的类以及通过反射访问它。
public class PrivateObject {private String privateString = null;public PrivateObject(String privateString) {this.privateString = privateString;}private String getPrivateString(){return this.privateString;}
}PrivateObject privateObject = new PrivateObject("The Private Value");
Method privateStringMethod = PrivateObject.class.getDeclaredMethod("getPrivateString", null);privateStringMethod.setAccessible(true);String returnValue = (String)
privateStringMethod.invoke(privateObject, null);System.out.println("returnValue = " + returnValue);
打印出的结果来自于 PrivateObject
实例中私有方法 getPrivateString()
的调用结果。
returnValue = The Private Value
注意点和访问私有属性一样:
getDeclaredMethod()
存在 class 本身的范围限制,不能获取到父类中定义的任何方法- 需要调用
Method.setAcessible(true)
来关闭反射中的Method
的访问权限检查,确保即便不满足访问条件,亦能在反射中成功访问
了解完通过反射来访问私有属性、方法的知识之后,让我们用在 unit test 中来测试本来难以覆盖到的私有方法。
LoginPresenter.kt
比如,我们的代码库中存在如下类 LoginPresenter
,并且咱们想要去单元测试其私有方法 saveAccount()
。
class LoginPresenter @Inject constructor(private val view: LoginView,private val strategy: CancelStrategy,private val navigator: AuthenticationNavigator,private val tokenRepository: TokenRepository,private val localRepository: LocalRepository,private val settingsInteractor: GetSettingsInteractor,private val analyticsManager: AnalyticsManager,private val saveCurrentServer: SaveCurrentServerInteractor,private val saveAccountInteractor: SaveAccountInteractor,private val factory: RocketChatClientFactory,val serverInteractor: GetConnectingServerInteractor
) {private var currentServer = serverInteractor.get() ?: defaultTestServerprivate val token = tokenRepository.get(currentServer)private lateinit var client: RocketChatClientprivate lateinit var settings: PublicSettingsfun setupView() {setupConnectionInfo(currentServer)setupForgotPasswordView()}private fun setupConnectionInfo(serverUrl: String) {currentServer = serverUrlclient = factory.get(currentServer)settings = settingsInteractor.get(currentServer)}private fun setupForgotPasswordView() {if (settings.isPasswordResetEnabled()) {view.showForgotPasswordView()}}fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {launchUI(strategy) {view.showLoading()try {val token = retryIO("login") {when {settings.isLdapAuthenticationEnabled() ->client.loginWithLdap(usernameOrEmail, password)usernameOrEmail.isEmail() ->client.loginWithEmail(usernameOrEmail, password)else ->client.login(usernameOrEmail, password)}}val myself = retryIO("me()") { client.me() }myself.username?.let { username ->val user = User(id = myself.id,roles = myself.roles,status = myself.status,name = myself.name,emails = myself.emails?.map { Email(it.address ?: "", it.verified) },username = username,utcOffset = myself.utcOffset)localRepository.saveCurrentUser(currentServer, user)saveCurrentServer.save(currentServer)localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)saveAccount(username)saveToken(token)analyticsManager.logLogin(AuthenticationEvent.AuthenticationWithUserAndPassword,true)view.saveSmartLockCredentials(usernameOrEmail, password)navigator.toChatList()}} catch (exception: RocketChatException) {when (exception) {is RocketChatTwoFactorException -> {navigator.toTwoFA(usernameOrEmail, password)}else -> {analyticsManager.logLogin(AuthenticationEvent.AuthenticationWithUserAndPassword,false)exception.message?.let {view.showMessage(it)}.ifNull {view.showGenericErrorMessage()}}}} finally {view.hideLoading()}}}fun forgotPassword() = navigator.toForgotPassword()private fun saveAccount(username: String) {val icon = settings.favicon()?.let {currentServer.serverLogoUrl(it)}val logo = settings.wideTile()?.let {currentServer.serverLogoUrl(it)}val thumb = currentServer.avatarUrl(username, token?.userId, token?.authToken)val account = Account(settings.siteName() ?: currentServer,currentServer,icon,logo,username,thumb)saveAccountInteractor.save(account)}private fun saveToken(token: Token) = tokenRepository.save(currentServer, token)
}
LoginPresenterTest.kt
单元测试的整体如下:
class LoginPresenterTest {private val view = mock(LoginView::class.java)private val strategy = mock(CancelStrategy::class.java)private val navigator = mock(AuthenticationNavigator::class.java)private val tokenRepository = mock(TokenRepository::class.java)private val localRepository = mock(LocalRepository::class.java)private val settingsInteractor = mock(GetSettingsInteractor::class.java)private val analyticsManager = mock(AnalyticsManager::class.java)private val saveCurrentServer = mock(SaveCurrentServerInteractor::class.java)private val saveAccountInteractor = mock(SaveAccountInteractor::class.java)private val factory = mock(RocketChatClientFactory::class.java)private val serverInteractor = mock(GetConnectingServerInteractor::class.java)private val token = mock(Token::class.java)const val currentServer: String = "https://open.rocket.chat"const val USERNAME: String = "user121"const val PASSWORD: String = "123456"lateinit var loginPresenter: LoginPresenterprivate val account = Account(currentServer, currentServer, null,null, USERNAME, UPDATED_AVATAR)@Beforefun setUp() {MockitoAnnotations.initMocks(this)`when`(strategy.isTest).thenReturn(true)`when`(serverInteractor.get()).thenReturn(currentServer)loginPresenter = LoginPresenter(view, strategy, navigator, tokenRepository, localRepository, settingsInteractor,analyticsManager, saveCurrentServer, saveAccountInteractor, factory, serverInteractor)}@Testfun `check account is saved`() {...}
}
通过反射机制,私有方法 saveAccount() 的单测则可以很方便地进行。
class LoginPresenterTest {...@Testfun `check account is saved`() {loginPresenter.setupView()val method = loginPresenter.javaClass.getDeclaredMethod("saveAccount", String::class.java)method.isAccessible = trueval parameters = arrayOfNulls<Any>(1)parameters[0] = USERNAMEmethod.invoke(loginPresenter, *parameters)verify(saveAccountInteractor).save(account)}
}
本文浅显易懂,希望能向你展示反射的魔力,帮助开发者在单元测试中优雅、便捷地 cover 到私有方法!
最后,感谢你的阅读。
相关文章:

如何优雅地单元测试 Kotlin/Java 中的 private 方法?
翻译自 https://medium.com/mindorks/how-to-unit-test-private-methods-in-java-and-kotlin-d3cae49dccd ❓如何单元测试 Kotlin/Java 中的 private 方法❓ 首先,开发者应该测试代码里的 private 私有方法吗? 直接信任这些私有方法,测试到…...

单元测试,集成测试,系统测试的区别是什么?
实际的测试工作当中,我们会从不同的角度对软件测试的活动进行分类,题主说的“单元测试,集成测试,系统测试”,是按照开发阶段进行测试活动的划分。这种划分完整的分类,其实是分为四种“单元测试,…...
数据结构(超详细讲解!!)第十八节 串(KMP算法)
1.BF算法 算法在字符比较不相等,需要回溯(即ii-j1):即退到s中的下一个字符开始进行继续匹配。 最好情况下的时间复杂度为O(m)。 最坏情况下的时间复杂度为O(nm)。 平均的时间复杂度为O(nm)。 2.KMP算法 KMP算法是D.E.Knuth、…...

软考_软件设计师
算法: 1、直接插入排序 详解:https://blog.csdn.net/qq_44616044/article/details/115708056 void insertSort(int data[],int n){int i,j,temp;for(i1;i<n;i){if(data[i]<data[i-1]){temp data[i];data[i] data[i-1];for(ji-1;j>0&&am…...

大数据之LibrA数据库系统告警处理(ALM-12004 OLdap资源异常)
告警解释 当Manager中的Ldap资源异常时,系统产生此告警。 当Manager中的Ldap资源恢复,且告警处理完成时,告警恢复。 告警属性 告警参数 对系统的影响 Ldap资源异常,Manager和组件WebUI认证服务不可用,无法对Web上层…...

详解—数据结构《树和二叉树》
目录 一.树概念及结构 1.1树的概念 1.2树的表示 二.二叉树的概念及结构 2.1概念 2.2二叉树的特点 2.3现实中的二叉树 2.4数据结构中的二叉树 2.5 特殊的二叉树 2.6二叉树的存储结构 2.6.1二叉树的性质 2.6.2 顺序结构 2.6.3链式存储 三. 二叉树的链式结构的遍历 …...

菜单管理中icon图标回显
<el-table-column prop"icon" label"图标" show-overflow-tooltip algin"center"><template v-slot"{ row }"><el-icon :class"row.icon"></el-icon></template></el-table-column>...

Postman如何导出接口的几种方法
本文主要介绍了Postman如何导出接口的几种方法,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 前言: 我的文章还是一贯的作风,简确用风格(简单确实有用)&…...

Java进阶(Set)——面试时Set常见问题解读 结合源码分析
前言 List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。 本篇博客介绍常见的关于Java中Set集合的面试问题,结合源码分析题目背后的知识点。 关于List的博客文章如下: Java进阶(List)——面试时L…...

【强化学习】12 —— 策略梯度(REINFORCE )
文章目录 前言策略梯度基于策略的强化学习的优缺点Example:Aliased Gridworld策略目标函数策略优化策略梯度利用有限差分计算策略梯度得分函数和似然比策略梯度定理蒙特卡洛策略梯度(Monte-Carlo Policy Gradient)Puck World Example Softmax随机策略 代…...

Kubernetes Taint(污点) 和 Toleration(容忍)
Author:rab 目录 前言一、Taint(污点)1.1 概述1.2 查看节点 Taint1.3 标记节点 Taint1.4 删除节点 Taint 二、Toleration(容忍) 前言 Kubernetes 中的污点(Taint)和容忍(Toleration…...
使用cv::FileStorage时出错 Can‘t open file: yaml‘ in read mode
1. 使用说明 在做的一个c工程项目,想加一个配置文件,我发现主要有两种主流的方式, (1)opencv有cv::FileStorage这样的一个函数可以使用。 (2)也可以使用cpp-yaml GitHub - jbeder/yaml-cpp: …...
代码之困:那些让你苦笑不得的bug
在编写代码的过程中,我们常常会遇到各种各样的bug。有的时候,我们花费了大量的时间和精力去寻找问题的根源,但却找不到任何线索。然而,令人哭笑不得的是,有时候这些问题的解决方案却是如此简单,以至于我们不…...

【C语言初学者周冲刺计划】2.2用选择法对10个整数从小到大排序
目录 1解题思路: 2代码如下: 3运行结果: 4总结: 1解题思路: 首先利用一维数组和循环语句输入10个整数,然后利用双循环的嵌套进行比较大小,最后输出结果; 2代码如下: #include&…...
c++系列——智能指针
1.智能指针的使用及原理 1.1 RAII RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资…...
力扣日记10.30-【栈与队列篇】滑动窗口最大值
力扣日记:【栈与队列篇】滑动窗口最大值 日期:2023.10.30 参考:代码随想录、力扣 239. 滑动窗口最大值 题目描述 难度:困难 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只…...
docker与宿主机共享内存通信
docker与宿主机共享内存通信 docker中的进程要与宿主机使用共享内存通信,需要在启动容器的时候指定“–ipchost”选项。然后再编写相应的共享内存的程序,一个跑在宿主机上,另一个跑在docker上面。 宿主机程序准备 shm_data.h #ifndef _SH…...

A股风格因子看板 (2023.10 第13期)
该因子看板跟踪A股风格因子,该因子主要解释沪深两市的市场收益、刻画市场风格趋势的系列风格因子,用以分析市场风格切换、组合风格暴露等。 今日为该因子跟踪第13期,指数组合数据截止日2023-09-30,要点如下 近1年A股风格因子检验统…...
ORB-SLAM3算法2之EuRoc、TUM和KITTI开源数据集运行ORB-SLAM3生成轨迹并用evo工具评估轨迹
文章目录 0 引言1 数据和真值1.1 TUM1.2 EuRoc1.3 KITTI2 ORB-SLAM3的EuRoc示例2.1 纯单目的示例2.2 纯单目的轨迹评估2.3 纯双目的示例2.4 纯双目的轨迹评估2.5 单目和IMU的示例2.6 单目和IMU的轨迹评估2.7 双目和IMU的示例2.8 双目和IMU的轨迹评估2.9 前四种的评估结果对比3 …...

【蓝桥杯选拔赛真题07】C++小球自由落体 青少年组蓝桥杯C++选拔赛真题 STEMA比赛真题解析
目录 C/C++小球自由落体 一、题目要求 1、编程实现 2、输入输出 二、算法分析...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...