Android单元测试(七):Robolectric,在JVM上调用安卓的类

今天讲讲Android上做单元测试的最后一个难点,那就是在JVM上无法调用安卓相关的类,不然的话,会报类似于下的错误: java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked. 关于这个话题,其实我以前是写过的,也许今天我回过头来写这个话题,会采用不一样的形式,不一样的心态来写,然而,作为我写过的第一篇关于单元测试的文章,而且看看时间,是去年的6月15号,再过几天,刚好一周年。想想这篇文章是在我刚开始探索,尝试在安卓上面写单元测试的时候,写的一篇文章,如今因为安卓单元测试的原因,我认识了很多同行,甚至不时有人叫我“大牛大神”之类的,虽然知道大家是客气,我也受之有愧,但怎么滴心里也有点虚荣的开心,哈哈哈。。因此现在回过头去看看当时自己写的东西,不禁觉得有点那啥。。。因此,我决定把之前的文章稍作补充和修改,作为这个系列的第七篇。 ----------------------以下文字写于去年今天----------------------- 作为一只本科非计算机专业的程序猿,手动写单元测试是我从来没接触过的东西,甚至在几个月前,我都不知道单元测试是什么东西。倒不是说没听过这个词,也不是不知道它的大概是什么东西——“用来测试一个方法,或者是一小块代码的测试代码”。然而真正是怎么做的?我并没有一个概念,或者说并没有一个感觉。 记得第一份工作在创新工场的时候,听当时的boss说,公司有个神级的程序员,他会写大量的单元测试,甚至50%以上的代码都是单元测试。当时崇拜之极,却仍然觉得写单元测试是很麻烦的一件事情。 扯远了,话说回来,当你接触多了国外的技术博客,视频之后,你会发现,单元测试甚至TDD,在国外是非常流行的事情。很多人甚至说离开了单元测试,他们便没有办法写代码。这些都让我对单元测试的好感度逐渐的上升。然而,真正让我下定决心,一定要研究一下这个东西的,是前段时间看大名鼎鼎的《重构:改善现有代码的艺术》里面的一段话: I've found that writing good tests greatly speeds my programming, even if I'm not refactoring. This was…

Continue ReadingAndroid单元测试(七):Robolectric,在JVM上调用安卓的类

Android单元测试(六):使用dagger2来做依赖注入,以及在单元测试中的应用

注: 代码中的 //<= 表示新加的、修改的等需要重点关注的代码 Class#method表示一个类的instance method,比如 LoginPresenter#login 表示 LoginPresenter的login(非静态)方法。 问题 在前一篇文章中,我们讲述了依赖注入的概念,以及依赖注入对单元测试极其关键的重要性和必要性。在那篇文章的结尾,我们遇到了一个问题,那就是如果不使用DI框架,而全部采用手工来做DI的话,那么所有的Dependency都需要在最上层的client来生成,这可不是件好事情。继续用我们前面的例子来具体说明一下。 假设有一个登录界面,LoginActivity,他有一个LoginPresenter,LoginPresenter用到了UserManager和PasswordValidator,为了让问题变得更明显一点,我们假设UserManager用到SharedPreference(用来存储一些用户的基本设置等)和UserApiService,而UserApiService又需要由Retrofit创建,而Retrofit又用到OkHttpClient(比如说你要自己控制timeout、cache等东西)。 应用DI模式,UserManager的设计如下: public class UserManager { private final SharedPreferences mPref; private final UserApiService mRestAdapter; public UserManager(SharedPreferences preferences, UserApiService userApiService)…

Continue ReadingAndroid单元测试(六):使用dagger2来做依赖注入,以及在单元测试中的应用

Android单元测试(五):依赖注入,将mock方便的用起来

在上一篇文章中,我们讲了要将mock出来的dependency真正使用起来,需要在测试环境下通过某种方式set 到用到它的那个对象里面进去,替换掉真实的实现。我们前面举的例子是: public class LoginPresenter { private UserManager mUserManager = new UserManager(); public void login(String username, String password) { //。。。some other code mUserManager.performLogin(username, password); } } 在测试LoginPresenter#login()时,为了能够将mock出来的UserManager set到LoginPresenter里面,我们前面的做法是简单粗暴,给LoginPresenter加一个UserManager的setter。然而这种做法毕竟不是很优雅,一般来说,我们正式代码里面是不会去调用这个setter,修改UserManager这个对象的。因此这个setter存在的意义就纯粹是为了方便测试。这个虽然不是没有必要,却不是太好看,因此在有选择的情况下,我们不这么做。在这里,我们介绍依赖注入这种模式。 对于依赖注入(Dependency Injection,以下简称DI)的准确定义可以在这里找到。它的基本理念这边简单描述下,首先这是一种代码模式,这个模式里面有两个概念:Client和Dependency。假如你的代码里面,一个类用到了另外一个类,那么前者叫Client,后者叫Dependency。结合上面的例子,LoginPresenter用到了UserManager,那么LoginPresenter叫Client,UserManager叫Dependency。当然,这是个相对的概念,一个类可以是某个类的Dependency,却是另外一个类的Client。比如说如果UserManager里面用到了Retrofit,那么相对于Retrofit,UserManager又是Client。DI的基本思想就是,对于Dependency的创建过程,并不在Client里面进行,而是由外部创建好,然后通过某种方式set到Client里面。这种模式,就叫做依赖注入。…

Continue ReadingAndroid单元测试(五):依赖注入,将mock方便的用起来

2016年第一次百日计划总结

今年年初,1月2日,我拉了一个群,跟其他24个小伙伴一起做了件事,叫100天计划。这是在“warfalcon”这个公众号看到的一种做法,基本大意就是在100天之内,每天坚持做同一件事情。关于100天计划的具体说明可以在这里跟这里找到。其实这也不是我第一次做这件事情了,去年的时候就执行过一次,当时给自己定的项目是每天看英文书局1个小时。但是那次执行的比较混乱,最后花了130多天才完成,也没有记录得很好。 这次我给自己定的项目是“每天阅读或写作一个小时”,依然不是100%的坚持,很多时候也会偷懒,最后花了114天完成。最后坚持下来的有10个人,其中有3个小伙伴做到了100%坚持,每一天都完成了,也没有请过一天假,真是神一样的存在。 Anyway,我在这期间做完的事情如下: ## 看书 看完了的(后面是评分) Learned Optimism ☆☆☆☆☆ 被讨厌的勇气 ☆☆☆☆☆ 楚亡 ☆☆☆☆☆ 野火集 ☆☆☆☆☆ Matz程序世界 ☆☆☆☆ 旅行的意义 ☆☆☆☆ 月亮与六便士 ☆☆☆☆ 领导梯队 ☆☆☆☆ 亲爱的安德烈 ☆☆☆☆ 没看完的(后面是看完的部分) Android框架解密(40%) Clean code(40%) 说话的魅力(50%)…

Continue Reading2016年第一次百日计划总结

Android单元测试(四):Mock以及Mockito的使用

几点说明: 代码中的 //<== 表示跟上面的相比,这是新增的,或者是修改的代码,不知道怎么样在代码块里面再强调几行代码 T_T。。。 很多时候,为了避免中文歧义,我会用英文表述 在第一篇文章里面我们提到,返回类型为 void 方法的单元测试方式,往往是验证里面的某个对象的某个方法是否得到了调用。在那篇文章里面,我举的例子是 activity 里面的一个 login 方法: public void login() { String username = ...//get username from username EditText String password = ...//get password…

Continue ReadingAndroid单元测试(四):Mock以及Mockito的使用

Android单元测试在蘑菇街支付金融部门的实践

大家好,我是蘑菇街支付金融部门的邹勇,花名叫小创。今天很高兴跟大家分享一下安卓的单元测试在蘑菇街支付金融的实践。下面,我们从为什么开始。 为什么要写单元测试 首先要介绍为什么蘑菇街支付金融这边会采用单元测试的实践。说起来比较巧,刚开始的时候,只是我一个人会写单元测试。后来老板们知道了,觉得这是件很有价值的事情,于是就叫我负责我们组的单元测试这件事情。就这样慢慢的,单元测试这件事情就成了我们这边的正常实践了。再后来,在公司层面也开始有一定的推广。 要说为什么要写单元测试的话,我相信大部分人都能承认、也能理解单元测试在保证代码质量,防止bug或尽早发现bug这方面的作用,这可能是大家觉得单元测试最大的作用。然而我觉得,除了这方面的作用,单元测试还能在很大程度上改善代码的设计,同时还能节约时间,让人工作起来更自信、更开心,以及其他的一些好处。这些都是我的切身感受,我相信也是多数真正实践过单元测试的人的切身感受,而不是为了宣传这个东西而说的好听的大话。 说到节约时间,大家可能就会好奇了,写单元测试需要时间,维护单元测试代码也需要时间,应该更费时间才对啊? 这就是在开始分享之前,我想重点澄清的一点,那就是,单元测试本身其实不会占用多少时间,相反,还会节约时间。只是:1. 学习如何做单元测试需要时间;2. 在一个没有单元测试的项目中加入单元测试,需要一定的结构调整的时间,因为一个有单元测试跟没有单元测试的项目,结构上还是有较大不同的。 打个比方,开车这件事情,需要很多时间吗?我相信很少人会说开车这件事情需要很多时间,而是:1. 学习开车,需要一定的时间;2. 如果路面不平的话,那么修路需要一定的时间。单元测试也是类似的情况。 那为什么说单元测试可以节约时间呢?简单说几点:1. 如果没有单元测试的话,就只能把app运行起来测试,这比运行一次单元测试要慢多了。2. 尽早发现bug,减少了debug和fixbug的时间。3. 重构的时候,大大减少手动验证重构正确性的时间。 所以,我希望大家能去掉"没时间写单元测试"这个印象,如果工作上安排太紧,没有时间学习如何做单元测试的话,可以自己私底下学,然后在慢慢应用到项目中。 单元测试简单介绍,以及void方法怎么测 接下来介绍我们这边是怎么做安卓单元测试的。首先澄清一下概念,在安卓上面写测试,有很多技术方案。有JUnit、Instrumentation test、Espresso、UiAutomator等等,还有第三方的Appium、Robotium、Calabash等等。我们现在讲的是使用JUnit和其他的一些框架,写可以在我们开发环境的JVM上面直接运行的单元测试,其他的几种其实都不属于单元测试,而是集成测试或者叫Functional test等等。这两者明显的不同是,前者可以直接在开发用的电脑,或者是CI上面的JVM上运行,而且可以只运行那么一小部分代码,速度非常快。而后者必须要有模拟器或真机,把整个project打包成一个app,然后上传到模拟器或真机上,再运行相关的代码,速度相对来说慢很多。 单元测试的定义相信大家都知道,就是为我们写的某一个代码单元(比如一个方法)写的测试代码。一个单元测试大概可以分为三个部分: setup:即new 出待测试的类,设置一些前提条件 执行动作:即调用被测类的被测方法,并获取返回结果 验证结果:验证获取的结果跟预期的结果是一样的 然而一个类的方法分两种,一种是有返回值的方法。一种是没有返回值的方法,即void方法。对于有返回值的方法,固然测试起来是很容易的,但是对于没有返回值的方法,该怎么测试呢?这里的关键是,怎么样获取这个方法的“返回结果”? 这里举一个例子来说明一下,顺便澄清一个十分常见的误解。比如说有一个Activity,管他叫DataActivity,它有一个public void loadData()方法, 会去调用底层的DataModel类,异步的执行一些网络请求。当网络请求返回以后,更新用户界面。…

Continue ReadingAndroid单元测试在蘑菇街支付金融部门的实践

Android单元测试(三):JUnit单元测试框架的使用

我们写单元测试,一般都会用到一个或多个单元测试框架,在这里,我们介绍一下 JUnit4 这个测试框架。这是 Java 界用的最广泛,也是最基础的一个框架,其他的很多框架,包括我们后面会看到的 Robolectric,都是基于或兼容 JUnit4 的。 然而首先要解决的问题是。。。 为什么要使用单元测试框架 或者换句话说,单元测试框架能够为我们做什么呢? 从最基本的开始说起,假如我们有这样一个类: public class Calculator { public int add(int one, int another) { // 为了简单起见,暂不考虑溢出等情况。 return one + another; }…

Continue ReadingAndroid单元测试(三):JUnit单元测试框架的使用

Android单元测试(二):再来谈谈为什么

今天早上8点半坐到桌子前,打开电脑,看了几分钟体育新闻,做其他一些准备工作,到9点开始真正开始着手写这篇文章。于是开始google,找资料,打算列一大段冠冕堂皇的理由,来说明为什么要写单元测试,比如: 对软件质量的提升 方便重构 节约时间 提升代码设计 。。。 等等等等。 然而我发现上面提到的几点,都不是很好解释。首先,我并没有具体的数据,来说明有了单元测试,我们的app crash率降了多少,bug少了多少等等。这种东西首先我们没有去衡量,因为单元测试的增加是循序渐进的,每个版本的迭代增加一点点。很难,我们也没有,去前后对比。再次,crash率的降低和bug的减少,也难以证明就是单元测试的作用。另外,像重构这种理由,怎么举例证明呢?例子小了显得没有意义,例子大了写起来很困难,读起来也困难。而关于节约时间,我也没有测量过,这个恐怕也很难去测量。只能从理论上去说明,为什么可以节约时间,恐怕也很难有说服力的去论述。同样的,对于代码设计的提升,也很难有力的去证明。 更重要的原因是,上面提到的种种好处,好像其实并不是我之所以要写单元测试的直接原因,更多的,他们像是一种结果。所以如果从列举和证明单元测试的好处这个角度去说明为什么要写单元测试的话,我感觉甚至很难说服我自己。 那就从自身的经历和感受去说说,我为什么要写单元测试吧。其实我之所以要写单元测试,或者说这么喜欢单元测试这种写代码的方式,是出于我自身的原因,或者说因为自身的一些缺点,让我走上了单元测试这条路,而且再也不想回头。 我为什么写单元测试 首先,是因为我不够自信 我相信大家都有接手,或者说参与到一个新项目的经历,也许是因为换了工作,也许是因为职位调动,或其他原因。当我拿到一个新项目的时候,会有一种诚惶诚恐的感觉,因为一时间比较难理清楚整个app的结构是怎么划分的,各部分各模块之间又是什么样的关系。我怕我改了某一个地方,结果其他一个莫名其妙的地方的受到了影响,然后导致了一个bug。这对于用户群大的app,尤其严重。所以,那种时候就会希望,如果我改了某个地方,能有个东西告诉我,这个改动影响到哪些地方,这样改是不是有问题的,会不会导致bug。虽然我可以把app启动起来,看看是不是能正常工作,然而一种case能工作,并不代表所有影响到的case都能工作。尤其是在不知道有哪些地方用到了的情况下,我更加难以去遍历所有用到的地方,一个一个去验证这个改动有没有问题。哪怕我知道所有的case,这也是一个很痛苦很费时间的过程,而且很多的外部条件也很难满足,比如说需要什么样的网络条件,需要用户是会员等等。 在这种情况下,单元测试是才是最好的工具。首先,单元测试只是针对一个代码单元写的测试,保证一个代码单元的正确性总比保证整个app的正确性容易吧?遍历一个方法的所有参数和输出情况总比遍历一个app的所有用户场景容易吧?跑一次单元测试总比运行一次app快吧? 因此,在改现有的代码之前,我会先对要改的代码单元做好隔离,写好测试,再去改,改好以后跑一边单元测试,验证他们依然是通过的,这时候我才有信心,将代码合并进去。 同样的情况会发生在重构的时候,我是一个对烂代码不大有忍受能力的人,看到不好的代码,我会忍不住想要去重构,不然的话,没有办法写新的代码。而重构就会有风险。因为我不够自信,重构的时候,也会有一种诚惶诚恐的感觉。这时候如果有完备的单元测试的话,我就能知道我的这次重构到底破坏了哪些地方,是不是对的,这样相对来说,就会放心的多了。 因此,想用单元测试来保证代码的正确性,这个是我喜欢写单元测试的重要原因之一。 再次,是因为我没有耐心 对于有一定经验,有一定代码思想的人来说,当他拿到一个新的需求,他会先想想代码的结构,应该有那些类,那些组件,什么责任应该划分到哪里去,然后才开始动手写代码,这个是很自然的一个思维过程。然而在不写单元测试的情况下,我们可能要把整个feature都做完整,从model到controller(或Presenter、ViewModel)到view到util等等,一整套流程做下来,到最后才可能运行起来看看是不是对的,有的时候哪怕所有代码都写完了,也不一定能验证是不是对的,比如说后台还没有ready等等。总之,在没有单元测试的情况下,我们需要等到最后一刻才能手动验证代码是不是对的,然后发现原来这里错了一点,那里少了一点,然后一遍一遍的把app运行起来,改一点运行一遍。。。 当我开始写单元测试之后,我发现这个过程实在是太漫长了,我喜欢写完一部分功能独立的代码,就能立刻看到他们是不是正确的。如果不是的话,我可以立刻就改正,而不用等到所有代码都写完整。要达到这点,那就只有写单元测试了。 当然,哪怕有单元测试,最后还是要做一遍手动测试工作,然而因为前面我已经保证每一个单元都是对的,最后只不过是验证每一部分都是正确的串联起来了而已,这点相对来说,是很容易的。所以最后所需要的手动测试,可以少很多,顺利很多,也简单得多。 最后,是因为我懒 如前所述,如果没有单元测试的话,那就只有手工测试,把app运行起来,如果有错的话,改一点东西,再运行起来。。。这个过程太漫长太痛苦,对于一个很懒的人来说,如果能写代码来代替手工测试,每次写完代码只需要按一次快捷键,就可以直接在IDE里面看到结果,那是多爽的一件事!所以冲着这点,我也不想回头。 我记得上一次使用“把app运行起来”这种开发方式,还是因为调试一个动画效果。因为动画效果是很难单元测试的,那就只有改一点代码,跑一边app,觉得不对,再改一点,跑一边,这样来来回回反反复复,那感觉真是。。。 单元测试给我带来了什么 前面讲了为什么我要写单元测试的原因,接下来讲讲用了单元测试这种写代码的方式以后,给我带来什么样的好处。这根前面讲的“原因”有部分重合的地方,然而也有不一样的地方。 更快的结果反馈 这点前面讲过了,有单元测试的帮助,我可以写完一个独立的代码单元,就立刻验证它的正确性,这跟需要完成所有代码再把app运行起来手动测试相比,是一个更快的反馈循环,能更快的发现代码是否正确,也更快的得到一种成就感。 更少的bug,或者说更快的发现bug…

Continue ReadingAndroid单元测试(二):再来谈谈为什么

Android单元测试: 首先,从是什么开始

这是一系列安卓单元测试的文章,目测主要会 cover 以下的主题: 什么是单元测试 为什么要做单元测试 JUnit Mockito Robolectric Dagger2 一个具体的app例子实践 神秘的bonus 什么是单元测试 首先需要介绍一下什么是单元测试。很多人像我一样,本科并不是计算机专业出身的,如果在职的公司不要求做单元测试的话,可能对这个词并没有一个确切的概念。而即使是计算机专业出身,如果毕业以后写的不多的话,可能对这个词的含义也不是很清楚。从名字上看,单元测试是为了测试某一个代码单元而写的测试代码。但是什么叫“一个代码单元”呢?是一个模块、还是一个类、还是一个方法(函数)呢?不同的人、不同的语言,都有不同的理解。一般的定义,尤其是是在OOP领域,是一个类的一个方法。在此,我们也这样理解:单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。 我们举一个例子说明一下,假如你有一个类,定义如下: public class Calculator { public int add(int one, int another) { //为了简单起见,暂不考虑溢出等情况。 return one + another;…

Continue ReadingAndroid单元测试: 首先,从是什么开始

命名是件大事情

这是《Clean code》第二章的笔记总结,关于程序变量、方法、类的命名的一些建议。文章的内容还是非常有帮助的。很多的建议虽然我们都听过,然而看了其中的例子,你就会发现,那些建议都并不是一句空话,这样写和那样写,可读性就是有很大不同。 ### 使用的名字要表现出它的作用 不好的命名:int d; // elapsed time in days. 好的命名: int elapsedTimeInDays; 很多时候,为了更好的可读性,可以使用一个类来封装一下基础类型,比如一个扫雷程序,游戏中每个格子可以用一个int[]来表示,其中数组中的元素表示了这个格子的坐标、状态等其他信息。下面的函数用于获取棋局中所有被点开的格子。 public List<int[]> getFlaggedCells() { List<int[]> flaggedCells = new ArrayList<int[]>(); for (int[] cell : gameBoard) if…

Continue Reading命名是件大事情