说起自动化测试只能用“又恨又爱”,起初接触自动化测试的时候是感入职没多久,公司正在弄自动化测试平台,那时候正需要一批小白鼠,而我们就成为了这些小白鼠,当时面对着覆盖率的压力,对之“恨之入骨”,但是还是度过了那段难熬的时间,而后续进入项目组中由于Get这项技能,在每次遇到不稳定复现的问题都会很容易想起编写自动化测试用例来
复现问题,这些自动化测试用例的好处是在解决完Bug的时候还可以使用这些自动化测试用例来验证时候完全解决了这个问题。但是对测试用例的看法还是:自动化是为开发维护服务的,而不能作为一个KPI强制来完成,因为这些自动化测试用例一般也是需要维护时间成本的,不要为了覆盖率强制写那些充数的自动化测试,好了开始今天的话题吧,
今天将会学习使用Robolectric进行单元测试。为了与时俱进这篇博客也升级到了Robolectric 3.0.Android Studio Preiview 2.2.

单元测试

单元测试中的单元指的是测试的最小模块,通常指的是某些类中的函数。一般有如下三类:
1.有明确的返回值,这类单元测试的时候只需要构造合适的参数,调用对应的方法,获取返回值,比对返回值是否和期待的返回值一致。
2.没有对应的返回值。只改变某些对象的一些属性或者状态,这时候就只能验证这些改变的属性和返回值就可以了。如果没有对象的属性被改变那么只能间接通过验证行为来验证了。
3.没有返回值,没有属性变化,没有外在行为改变的三无方法是无法测试的。
当前的主流单元测试框架有AndroidTest和Robolectric,前者需要运行在Android设备上而后者的最大特点是它:能让你的测试代码运行在JVM上,这就意味着在做单元测试的时候,不必将你的apk安装到机器上运行从而极大得减少了我们通过测试来验证问题的时间。

在一般Android项目开发中我们会使用单元测试来测试函数行为,四大组件状态,控件状态行为,在一些项目中往往会认为单元测试覆盖率越高越好,甚至拿它来作为一个硬性的指标,但是这是不大现实的,首先未必所有的项目周期都允许花那么长的时间来单独做自动化测试。有些函数逻辑并不复杂,并不需要作为测试对象进行测试。

一般我们在写测试用例之前需要确定要测试的测试点,可以用一个列表的形式罗列出测试点,针对每个测试点罗列出所属的测试页面,涉及的界面元素,较为详细的描述,输入数据和输出数据。
在确定测试项列表后就可以进行针对某个页面进行测试了,在进行测试之前需要找到当前页面的入口,并确定跳转到该页面的条件,这些条件一般会写到@Before中。然后分析当前页面的界面元素,业务逻辑,控件行为。在熟悉了上述的逻辑后,就可以建立测试用例对其进行测试了。测试结束后还需要进行覆盖率的统计,看下是否还有未考虑的因素在内。直到上述流程全部完成。

搭建环境

在这种情况下如果要使用robolectric需要在模块的build.gradle中添加如下依赖:

testCompile "org.robolectric:robolectric:3.0"

在test/src目录下创建Robolectric 测试类

@RunWith(RobolectricTestRunner.class)
public class WelcomeActivityTest {
@Test
public void clickingLogin_shouldStartLoginActivity() {
WelcomeActivity activity =
Robolectric.setupActivity(WelcomeActivity.class);
activity.findViewById(R.id.login).performClick();
Intent expectedIntent = new Intent(activity, WelcomeActivity.class);
assertThat(shadowOf(activity).getNextStartedActivity())
.isEqualTo(expectedIntent);
}
}

这里需要用注释注明使用RobolectricGradleTestRunner,注意我们必须指定指向Gradle编译系统产生的BuildConfig.class.Roblectric使用这些常量来获取Gradle编译应用所使用的输出路径。如果没有这些常量,Robolectric将不能找到项目中的manifest, resources,和assets这些资源。

配置Robolectric

最主要的配置Robolectric的方式是通过 @Config注释,这些注释可以用在类级别和方法级别,并且方法级别的注释将会覆盖掉类级别的注释.

  • 配置 SDK Level
    默认情况下Robolectric将会在targetSdkVersion上运行你的测试代码,但是如果你想在不同的sdk等级上运行你的测试代码可以通过如下方式修改:
    @Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
    public class SandwichTest {
    @Config(sdk = Build.VERSION_CODES.KITKAT)
    public void getSandwich_shouldReturnHamSandwich() {
    }
    }
  • 配置Application Class
    Robolectric将会尝试创建在AndroidManifest中指定的Application类的实例,如果你想提供一个自定义的实现我们可以通过如下方式来指定。
    @Config(application = CustomApplication.class)
    public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
    }
    我们通常不需要直接产生ActivityController只需要使用Robolectric.buildActivity()来生成ActivityController,对于那些只需要一个实例化后的Actiivity的情况只需要如下的代码来获取:
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();

    Activity实例创建

    这个一般用于获取Activity实例
    MainActivity mainActivity = Robolectric.setupActivity(MainActivty.class);
    assertNotNull(mainActivity);
    assertEquals(mainActivity.getTitle(),"MyApplication");

    Activity跳转测试

    btn.performClick();
    Intent expectedIntent = new Intent(mainActitity,LoginActivity);
    intent actualIntent = ShadowAppication.getInstance().getNextStartedActivity();
    assetEquals(expectedIntent,actualIntent);

    测试Activity生命周期

    要测试Activity的生命周期可以通过如下的方式:
    ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
    Activity activity = controller.get();
    // assert that something hasn't happened
    activityController.resume();
    // assert it happened!
    通过相同的方式还可以测试start(), pause(), stop(), 和destroy()等生命周期。

如果需要测试使用Intent启动Activity可以借助如下方式进行:

Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
Bundle savedInstanceState = new Bundle();
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
.create()
.restoreInstanceState(savedInstanceState)
.get();

UI控件状态测试

CheckBox checkbox = (CheckBox)mainActivity.findViewById(R.id.checkbox);
Button btn = (Button)mainActivity.findViewById(R.id.btn)

checkbox.setChecked(true);
btn.performClick();
assertTrue(!checkbox.isChecked());

Dialog:

AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(dialog);

Toast:

ShadowToast.getTextOfLatestToast();

BroadcastReceiver的测试:

ShadowApplication shadowApplication = ShadowApplication.getInstance;
Intent intent = new Intent("xxxxxxxx");
assertTrue(shadowApplication.hasReceiverForInstance(intent));

MyReceiver myreceiver = new MyReceiver();
myreceiver.onReceiver(RuntimeEnvironment.application.intent);
测试调用接收器的结果

服务的测试:
MyService service = new MyService();
registrationService.onHandleIntent(new Intent());

资源的使用:

通过一个例子来介绍字符串的使用,下面包含三个目录下的strings.xml,

values/strings.xml
<string name="not_overridden">Not Overridden</string>
<string name="overridden">Unqualified value</string>
<string name="overridden_twice">Unqualified value</string>
values-en/strings.xml
<string name="overridden">English qualified value</string>
<string name="overridden_twice">English qualified value</string>
values-en-port/strings.xml
<string name="overridden_twice">English portrait qualified value</string>

下面使用了@Config(qualifiers=”en-port”)这样就会现在values-en-port中查找,如果没有的话就会在其他的string中找。

@Test
@Config(qualifiers="en-port")
public void shouldUseEnglishAndPortraitResources() {
final Context context = RuntimeEnvironment.application;
assertThat(context.getString(R.id.not_overridden)).isEqualTo("Not Overridden");
assertThat(context.getString(R.id.overridden)).isEqualTo("English qualified value");
assertThat(context.getString(R.id.overridden_twice)).isEqualTo("English portrait qualified value");
}
Shadow Classes

Robolectric 针对Android SDK中的对象定义了很多的shadow类,这些shadow类对Android原先的类进行了修改和扩展。当一个Android对象被实例化的时候Roblectric会寻找相应的shadow类。如果能够找到一个类,它将会产生一个shadow对象与之关联。每次在一个Android类中调用一个方法,Robolectric会确保shadow类的对应的方法会最先被调用。所以这些shadow类中的成员方法将都有一次运行机会。这个适用于所有包括静态和final的方法。

添加行为

如果shadow类提供的行为不是你需要的,但是也许只是在单个测试中,或者一组测试中,或者全部的测试集合这种提供的行为不适合,这时候我们可以简单地重新地声明一个类(比如ShadowFoo),然后在它上面添加@Implements(xxxx.class)这种注释。然后可以在测试方法或者类中添加
@Config(shadows=ShadowFoo.class)。

Shadow Classes

为了Robolectric框架能够实例化Shadow 类,Shadow类通常需要一个公共的非参数的构造函数,
Shadow classes 应该和对应类有对应的继承关系,比如我们需要实现一个ViewGroup的Shadow类,那么我们的shadow类需要继承ViewGroup的父类的Shadow,ShadowView。

@Implements(ViewGroup.class)
public class ShadowViewGroup extends ShadowView {

Shadow Methods

Shadow 对象中实现和Android类中有相同signature的方法,当Android对象的方法被调用的时候这个具有相同signature的方法将会同样被调用。
比如如下代码被调用的时候

this.imageView.setImageResource(R.drawable.pivotallabs_logo);

在对应的测试中ShadowImageView#setImageResource(int resId) 方法将会被调用
Shadow方法必须使用@Implementation进行注释

@Implements(ImageView.class)
public class ShadowImageView extends ShadowView {
...
@Implementation
public void setImageResource(int resId) {
// implementation here.
}
}

我们需要注意Shadow方法需要实现在对应的Shadow类中。否则Robolectric的查询机制将不会找到他们。比如setEnabled()方法是在View中定义.如果setEnabled()在ShadowViewGroup中定义而不是在ShadowView中定义,它将不会被调用。

Shadowing Constructors

一旦Shadow对象被实例化,Robolectric将会寻找和实际类中具有相同参数的的构造函数的__constructor__方法。比如在应用代码中触发了一个接收一个Context的TextView构造方法那么对应的Robolectric将会触发接收一个Context的__constructor__ 方法。

@Implements(TextView.class)
public class ShadowTextView {
...
public void __constructor__(Context context) {
this.context = context;
}
...

Getting access to the real instance

有时Shadow类可能会需要访问他们正在shadowing的实例对象,比如我们需要操纵对应的成员变量。 Shadow 类可以通过声明一个成员变量并使用@RealObject对其进行注释:

@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}

在上面的例子中Robolectric 将会在触发任何方法之前将realPoint赋给Point的实例对象。需要注意的是在实际对象中调用的方法依旧会被拦截和重定向。这个特性在测试代码中并没什么影响,但是它在Shadow类的实现中将会有很大影响。

创建自定义Shadows

自定义shadows是Robolectric一个重要的特性,它允许我们修改目标测试Android功能的行为。自定义shadows允许我们在不添加或者修改Robolectric提供的Shadow代码的情况下修改某个方法的行为,它也允许我们的shadow指定的context。

编写自定义 Shadow

自定义shadows在结构上和正常的shadow类有很大的相似性。它必须在类定义中包含@Implements(AndroidClassName.class) 注释,我们可以使用正常的implementation 选项,比如使用@Implementation 实例化方法,或者使用

public void __constructor__(...).

实例化一个构造方法。

@Implements(Bitmap.class)
public class MyShadowBitmap {
@RealObject private Bitmap realBitmap;
private int bitmapQuality = -1;

@Implementation
public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
bitmapQuality = quality;
return realBitmap.compress(format, quality, stream);
}

public int getQuality() {
return bitmapQuality;
}
}
}

自定义TestRunner

public class CustomShadowTestRunner extends RobolectricGradleTestRunner {
public CustomShadowTestRunner(Clas<?> cls) throws initializationError {
super(cls);
}

public InstrumentationConfiguration creatClassLoaderConfig(){
InstrumentationConfiguration.Builder builder = Instrumentation.newBuilder();
builder.addInstrumentatedClass(OriginClass.class.getName());
return builder.build();
}

}

使用自定义Shadow

自定义 Shadows 使用@Config注释和对应的测试类或者测试方法绑定。如果需要使用在上面介绍的MyShadowBitmap类我们需要使用@Config(shadows={MyShadowBitmap.class})这种方式来包含或者使用
@Config(shadows={MyShadowBitmap.class, MyOtherCustomShadow.class})这种方式来包含多个Shadow类。这将会让Robolectric识别并在测试你对应的方法的时候使用这些自定义的Shadow。

@RunWith(CustomShadowTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowPerson.class})
public class ShadowTest {
@Test
public void testCustomShadowdemo(){
Person person = new Person("CustomShadowTestRunner");
person.xxxxxx //由于绑定了Shadow所以实际上调用的是ShadowPerson对应的方法

//获取对应的Shadow对象
ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);
shadowPerson.xxxxxxx //调用ShadowPerson对应的方法

}
}

使用Log来调试我们的测试用例

要使用Log功能只需要在每个TestCase的setUp()里执行ShadowLog.stream = System.out即可,

@Before
public void setUp() throws URISyntaxException {
ShadowLog.stream = System.out;
}

关于网络与数据库的测试

如果要学习网络与数据库的测试大家可以移步如下链接:
并且作者有提供了一份很好的学习代码可供学习,个人觉得结合googeSample的android-testging和作者的测试例子就可以较好得掌握测试用例的编写了。

关于测试用例学习的一些较好的源码

这些文章只能帮助我们对一些概念的理解,但是对于程序员来说有一份实际可用的源码进行学习是最好不过了,下面是一些我在学习自动化测试部分时候收集的一些源码。分享给大家,大家可以让这些代码跑起来,在他们的基础上做一些实验就可以更好得掌握自动化测试用例的编写了。

https://github.com/googlesamples/android-testing
https://github.com/geniusmart/LoveUT

较好的文章

[http://www.jianshu.com/p/9d988a2f8ff7]http://www.jianshu.com/p/9d988a2f8ff7
[http://www.jianshu.com/p/3aa0e4efcfd3]http://www.jianshu.com/p/3aa0e4efcfd3
[http://tech.meituan.com/Android_unit_test.html]http://tech.meituan.com/Android_unit_test.html

Contents
  1. 1. 单元测试
  2. 2. 搭建环境
  3. 3. 配置Robolectric
  4. 4. Activity实例创建
  5. 5. Activity跳转测试
  6. 6. 测试Activity生命周期
  7. 7. UI控件状态测试
  8. 8. 资源的使用:
    1. 8.1. Shadow Classes
  9. 9. 添加行为
  10. 10. Shadow Classes
  11. 11. Shadow Methods
  12. 12. Shadowing Constructors
  13. 13. Getting access to the real instance
  14. 14. 创建自定义Shadows
  15. 15. 编写自定义 Shadow
  16. 16. 自定义TestRunner
  17. 17. 使用自定义Shadow
  18. 18. 使用Log来调试我们的测试用例
  19. 19. 关于网络与数据库的测试
  20. 20. 关于测试用例学习的一些较好的源码
  21. 21. 较好的文章