Android单元测试总结

JUnit

Java语言单元测试框架,非常的基础和强大,主要用于断言。

官方文档

配置

testImplementation 'junit:junit:4.13.2'

Assert类中的常用方法

方法名方法描述
assertEquals断言传入的预期值与实际值是相等的
assertNotEquals断言传入的预期值与实际值是不相等的
assertArrayEquals断言传入的预期数组与实际数组是相等的
assertNull断言传入的对象是为空
assertNotNull断言传入的对象是不为空
assertTrue断言条件为真
assertFalse断言条件为假
assertSame断言两个对象引用同一个对象,相当于“==”
assertNotSame断言两个对象引用不同的对象,相当于“!=”
assertThat断言实际值是否满足指定的条件

注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。

常用注解

注解名含义
@Test表示此方法为测试方法
@Before在每个测试方法前执行,可做初始化操作
@After在每个测试方法后执行,可做释放资源操作
@Ignore忽略的测试方法
@BeforeClass在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass在类中最后运行。此注解修饰的方法必须是static void
@RunWith指定该测试类使用某个运行器
@Parameters指定测试类的测试数据集合
@Rule重新制定测试类中方法的行为
@FixMethodOrder指定测试类中方法的执行顺序

执行顺序:@BeforeClass -> @Before –> @Test –> @After –> @AfterClass

示例

基础用法

class JunitTest {
  @Test
  fun test() {
    assertEquals(4, 2+2) //验证 期望值 与 真实值 是否一致
  }
  @Test(expected = IOException::class) //校验是否抛出期望的异常
  @Throws(Exception::class)
  fun testException() {
    throw IOException("test io exception")
  }
}

参数化测试

@RunWith(Parameterized::class)
class JunitTest(private val num: Int) {
  companion object {
    @JvmStatic
    @Parameterized.Parameters
    fun staticMethod() : List<Int> {
      return listOf(1+1, 2+2, 3+3)
    }
  }
  @Test
  fun test() {
    assertEquals(4, num) //输出结果:test[0]和test[2]校验失败,test[1]校验成功
  }
}

assertThat用法

常用的匹配器如下表:

匹配器说明例子
is断言参数等于后面给出的匹配表达式assertThat(5, `is`(5))
not断言参数不等于后面给出的匹配表达式assertThat(5, not(6))
equalTo断言参数相等assertThat(30, equalTo(30))
equalToIgnoringCase断言字符串相等忽略大小写assertThat("Ab", equalToIgnoringCase("ab"))
containsString断言字符串包含某字符串assertThat("abc", containsString("bc"))
startsWith断言字符串以某字符串开始assertThat("abc", startsWith("a"))
endsWith断言字符串以某字符串结束assertThat("abc", endsWith("c"))
nullValue断言参数的值为nullassertThat(null, nullValue())
notNullValue断言参数的值不为nullassertThat("abc", notNullValue())
greaterThan断言参数大于assertThat(4, greaterThan(3))
lessThan断言参数小于assertThat(4, lessThan(6))
greaterThanOrEqualTo断言参数大于等于assertThat(4, greaterThanOrEqualTo(3))
lessThanOrEqualTo断言参数小于等于assertThat(4, lessThanOrEqualTo(6))
closeTo断言浮点型数在某一范围内assertThat(4.0, closeTo(2.6, 4.3))
allOf断言符合所有条件,相当于&&assertThat(4,allOf(greaterThan(3), lessThan(6)))
anyOf断言符合某一条件,相当于或assertThat(4,anyOf(greaterThan(9), lessThan(6)))
hasKey断言Map集合含有此键assertThat(map, hasKey(“key”))
hasValue断言Map集合含有此值assertThat(map, hasValue(value))
hasItem断言迭代对象含有此元素assertThat(list, hasItem(element))
示例
class JunitTest {
  @Test
  fun test() {
    /* 输出信息如下:
    junit.framework.AssertionFailedError: custom error info
    Expected: is "bb"
         Got: was "aa"
      at androidx.test.espresso.matcher.ViewMatchers.assertThat(ViewMatchers.java:16)
     */
    assertThat("custom error info", "aa", `is`("bb"))
  }
}

@Rule用法

//MyRule.kt
class MyRule : TestRule {
  override fun apply(base: Statement?, description: Description?): Statement {
    return object: Statement() {
      override fun evaluate() {
        val methodName = description?.methodName // 获取测试方法的名字
        println(methodName + "测试开始!")
        // evaluate前执行方法相当于@Before
        base?.evaluate()  // 运行测试方法
        // evaluate后执行方法相当于@After
        println(methodName + "测试结束!")
      }
    }
  }
}
//JunitTest.kt
class JunitTest {
  @get:Rule
  public val rule = MyRule()
  @Test
  fun test() {
    //因为校验不通过,所以只会输出”测试开始!“,而不会输出”测试结束!“
    assertThat("custom error info", "aa", `is`("bb"))
  }
}

Mockito

解决测试类之间的耦合,可以mock相关的类。但是Mockito框架不支持mock匿名类、final类、static方法、private方法。

官方文档

配置

testImplementation 'org.mockito:mockito-core:3.9.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"

定义测试类

open class Person(open var name: String, open var age: Int) { 
  open fun eat(food: String): String {
    return food
  }
}

示例

四种Mock方式

//普通方法
class MockitoTest {
  @Test
  fun testIsNotNull() {
    val person = mock(Person::class.java)
    assertNotNull(person)
  }
}
//注解方法
class MockitoAnnotationsTest {
  @Mock
  lateinit var person: Person
  @Before
  fun setUp() {
    MockitoAnnotations.initMocks(this)
  }
  @Test
  fun testIsNotNull() {
    assertNotNull(person)
  }
}
//运行器方法
@RunWith(MockitoJUnitRunner::class) // 使用MockitoJUnitRunner
class MockitoJUnitRunnerTest {
  @Mock
  lateinit var person: Person
  @Test
  fun testIsNotNull() {
    assertNotNull(person)
  }
}
//MockitoRule方法
class MockitoRuleTest {
  @Mock
  lateinit var person: Person
  @JvmField
  @Rule
  var mockitoRule: MockitoRule = MockitoJUnit.rule()
  @Test
  fun testIsNotNull() {
    Assert.assertNotNull(person)
  }
}

常用打桩方法

因为Mock出的对象中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等,给Mock打基础。

方法名方法描述
thenReturn(T value)设置要返回的值
thenThrow(Throwable… throwables)设置要抛出的异常
thenAnswer(Answer<?> answer)对结果进行拦截
doReturn(Object toBeReturned)提前设置要返回的值
doThrow(Throwable… toBeThrown)提前设置要抛出的异常
doAnswer(Answer answer)提前对结果进行拦截
doCallRealMethod()调用某一个方法的真实实现
doNothing()设置void方法什么也不做
class MockitoRuleTest {
  @Mock
  lateinit var person: Person
  @JvmField
  @Rule
  var mockitoRule: MockitoRule = MockitoJUnit.rule()
  @Test
  fun testPersonAnswer() {
    Mockito.`when`(person.eat(Mockito.anyString())).thenAnswer {
      // 注意:这里Object[]对应的kotlin类型
      val allArgs:Array<out Any> = it.arguments
      // 将参数传递过来,并且将结果返回
      allArgs[0].toString() + "真好吃"
    }
    // 输出:饺子真好吃
    println(person.eat("饺子"))
  }
}

常用验证方法

前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。从概念上讲,就是和状态测试所不同的“行为测试”了。

方法名方法描述
after(long millis)在给定的时间后进行验证
timeout(long millis)验证方法执行是否超时
atLeast(int minNumberOfInvocations)至少进行n次验证
atMost(int maxNumberOfInvocations)至多进行n次验证
description(String description)验证失败时输出的内容
times(int wantedNumberOfInvocations)验证调用方法的次数
never()验证交互没有发生,相当于times(0)
only()验证方法只被调用一次,相当于times(1)
class MockitoRuleTest {
  @Mock
  lateinit var person: Person
  @JvmField
  @Rule
  var mockitoRule: MockitoRule = MockitoJUnit.rule()
  @Test
  fun testPersonVerifyAfter() {
    Mockito.`when`(person.age).thenReturn(18)
    // 延时1s验证
    println(person.age)
    println(System.currentTimeMillis())
    Mockito.verify(person, Mockito.after(1000)).age
    println(System.currentTimeMillis())
        //这里如果不再次调用person的age变量会校验失败,因为距离最近一次verify之后,age变量只被执行了一次
    println(person.age)
    Mockito.verify(person, Mockito.atLeast(2)).age
  }
}

常用参数匹配器

方法名方法描述
anyObject()匹配任何对象
any(Class<T> type)与anyObject()一样
any()与anyObject()一样
anyBoolean()匹配任何boolean和非空Boolean
anyByte()匹配任何byte和非空Byte
anyCollection()匹配任何非空Collection
anyDouble()匹配任何double和非空Double
anyFloat()匹配任何float和非空Float
anyInt()匹配任何int和非空Integer
anyList()匹配任何非空List
anyLong()匹配任何long和非空Long
anyMap()匹配任何非空Map
anyString()匹配任何非空String
contains(String substring)参数包含给定的substring字符串
argThat(ArgumentMatcher<T> matcher)创建自定义的参数匹配模式
//注意:`when`和whenever是等价的
class MockitoRuleTest {
  @Mock
  lateinit var person: Person
  @JvmField
  @Rule
  var mockitoRule: MockitoRule = MockitoJUnit.rule()
  @Test
  fun testPersonAny() {
    `when`(person.eat(any())).thenReturn("米饭")
    //输出米饭
    println(person.eat("面条"))
  }
  @Test
  fun testPersonArgThat() {
    //自定义输入字符长度为偶数时,输出面条
    whenever(person.eat(argThat(ArgumentMatcher {
      it.length % 2 == 0
    }))).thenReturn("面条")
    //输出面条
    println(person.eat("1234"))
  }
}

其他方法

方法名方法描述
reset(T… mocks)重置Mock
spy(Class<T> classToSpy)实现调用真实对象的实现
inOrder(Object… mocks)验证执行顺序
@InjectMocks注解自动将模拟对象注入到被测试对象中
/* 注意:这里直接使用前面的Person类会抛出异常:
  Unable to initialize @Spy annotated field 'mPerson'.
  Please ensure that the type 'Person' has a no-arg constructor.
  org.mockito.exceptions.base.MockitoException: Unable to initialize @Spy annotated field 'mPerson'.
  需要修改成下面这样:
  open class Person(open var name: String? = null, open var age: Int = 0) {
    open fun eat(food: String): String {
      return food
    }
  }
 */
class Home(val person: Person) {
  fun getMaster() : String? {
    return person.name
  }
}
class MockitoSpyTest {
  @Spy
  lateinit var mPerson: Person
  @InjectMocks
  lateinit var home: Home //依赖注入
  @JvmField
  @Rule
  val mockitoRule: MockitoRule = MockitoJUnit.rule()
  @Test
  fun testIsNotNull() {
    assertNotNull(mPerson)
  }
  @Test
  fun testPersonInOrder() {
    mPerson.name = "小明"
    mPerson.age = 1
    val inOrder = inOrder(mPerson)
    //这里会校验失败,应该先调用name变量再调用age变量
    inOrder.verify(mPerson).age = 1
    inOrder.verify(mPerson).name = "小明"
    //这里也会校验失败,因为age的值从1变成了0
    inOrder.verify(mPerson).name = "小明"
    inOrder.verify(mPerson).age = 0
  }
  @Test
  fun testHomeInjectMocks() {
    whenever(mPerson.name).thenReturn("coding")
    println(home.getMaster())
  }
}

PowerMock

Mockito的增强,PowerMock可以mock匿名类、final类、static方法、private方法

官方文档

配置

//Mockito
testImplementation 'org.mockito:mockito-core:3.9.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
//PowerMock
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9"
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
testImplementation "org.powermock:powermock-classloading-xstream:2.0.9"

忽略一些类

//忽略android的相关类,因为我们使用Robolectric处理了。
//忽略Mockito和Robolectric的相关类,因为我们不应该mock它们自己。
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
class PowerMockitoMethodTest { 
}

定义测试类

//Fruit.kt
abstract class Fruit(private val fruit: String = "水果") {
  fun getFruit(): String {
    return fruit
  }
}
//Banana.kt
open class Banana : Fruit() {
  companion object {
    private const val COLOR: String = "黄色的"
    @JvmStatic
    open fun getColor(): String {
      return COLOR
    }
  }
  open fun getBananaInfo(): String {
    return flavor() + getColor()
  }
  private fun flavor(): String {
    return "甜甜的"
  }
  final fun isLike(): Boolean {
    return true
  }
}

示例

mock静态方法

@RunWith(PowerMockRunner::class)
@PrepareForTest(value = [Banana::class, Banana.Companion::class])
class PowerMockitoMethodTest {
  @Test
  fun testStaticMethod() {
    PowerMockito.mockStatic(Banana.Companion::class.java)
    Assert.assertEquals("黄色的", Banana.getColor())
  }
  @Test
  fun testStaticMethod2() {
    val mock = PowerMockito.mock(Banana.Companion::class.java)
    Mockito.`when`(mock.getColor()).thenReturn("绿色的")
    Assert.assertEquals("绿色的", mock.getColor())
  }
  @Test
  fun testStaticMethod3() {
    /* 这句断言要想成功,一定要将测试类Banana.kt中的const关键字去除,原因如下(省略了无关编译代码):
      //存在const关键字的kotlin代码编译为Java代码之后的结果:
      public class Banana extends Fruit {
         private static final String COLOR = "黄色的";
         public static final class Companion {
            @JvmStatic
            @NotNull
            public String getColor() {
               return "黄色的";
            }
         }
      }
      //删除const关键字的kotlin代码编译为Java代码之后的结果:
      public class Banana extends Fruit {
         private static final String COLOR = "黄色的";
         public static final class Companion {
            @JvmStatic
            @NotNull
            public String getColor() {
               return Banana.COLOR;
            }
         }
      }
    */
    Whitebox.setInternalState(Banana::class.java, "COLOR", "绿色的")
    Assert.assertEquals("绿色的", Banana.getColor())
  }
}

mock私有方法

@RunWith(PowerMockRunner::class)
@PrepareForTest(Banana::class)
class PowerMockitoMethodTest {
  //修改私有方法的返回值
  @Test
  @Throws(Exception::class)
  fun testPrivateMethod() {
    val mock = PowerMockito.mock(Banana::class.java)
    PowerMockito.`when`(mock.getBananaInfo()).thenCallRealMethod()
    //这里的泛型指的是flavor方法的返回值类型
    PowerMockito.`when`<String>(mock, "flavor").thenReturn("苦苦的")
    Assert.assertEquals("苦苦的黄色的", mock.getBananaInfo())
    PowerMockito.verifyPrivate(mock).invoke("flavor")
  }
  //跳过私有方法
  @Test
  fun testSuppressPrivateMethod() {
    val mock = PowerMockito.mock(Banana::class.java)
    PowerMockito.`when`(mock.getBananaInfo()).thenCallRealMethod()
    PowerMockito.suppress(PowerMockito.method(Banana::class.java, "flavor"))
    Assert.assertEquals("null黄色的", mock.getBananaInfo())
  }
  /* 修改父类的私有变量,虽然成功了,但是会抛出警告:
    WARNING: An illegal reflective access operation has occurred
    WARNING: Illegal reflective access by org.powermock.reflect.internal.WhiteboxImpl (file:/Users/yalla0703/.gradle/caches/transforms-3/3ab79aa2365620a31ba0cca57da30b0c/transformed/jetified-powermock-reflect-2.0.9.jar) to method java.lang.reflect.Field.getFactory()
    WARNING: Please consider reporting this to the maintainers of org.powermock.reflect.internal.WhiteboxImpl
    WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
    WARNING: All illegal access operations will be denied in a future release
   */
  @Test
  @Throws(Exception::class)
  fun testChangeParentPrivate() {
    val banana = Banana()
    MemberModifier.field(Banana::class.java, "fruit").set(banana, "蔬菜")
    Assert.assertEquals("蔬菜", banana.fruit)
  }
}

mock final方法

@RunWith(PowerMockRunner::class)
@PrepareForTest(Banana::class)
class PowerMockitoMethodTest {
  @Test
  fun testFinalMethod() {
    val mock = PowerMockito.mock(Banana::class.java)
    PowerMockito.`when`(mock.isLike()).thenReturn(false)
    Assert.assertFalse(mock.isLike())
  }
}

mock 构造方法

@RunWith(PowerMockRunner::class)
@PrepareForTest(Banana::class)
class PowerMockitoMethodTest {
  @Test
  fun testNewClass() {
    val mock = PowerMockito.mock(Banana::class.java)
    PowerMockito.`when`(mock.getBananaInfo()).thenReturn("大香蕉")
    //如果new新对象,则返回这个上面设置的这个对象
    PowerMockito.whenNew(Banana::class.java).withNoArguments().thenReturn(mock)
    //new新的对象
    val banana = Banana()
    Assert.assertEquals("大香蕉", banana.getBananaInfo())
  }
}

PowerMockRule

//@RunWith(PowerMockRunner::class)
@PrepareForTest(Banana::class)
class PowerMockitoMethodTest {
  /* 当我们有多个测试框架时,使用@RunWith可能会出现占用问题,这个时候我们可以考虑使用PowerMockRule。另外需要注意的是:使用PowerMockRule需要保证以下两个依赖存在:
    testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9"
    testImplementation "org.powermock:powermock-classloading-xstream:2.0.9"
  */
  @JvmField
  @Rule
  val rule = PowerMockRule()
  /* 使用PowerMockRule替代@RunWith会抛出如下警告:
    WARNING: An illegal reflective access operation has occurred
    WARNING: Illegal reflective access by com.thoughtworks.xstream.core.util.Fields (file:/Users/yalla0703/.gradle/caches/transforms-3/453ccb14937225f6d452947ad9d9d05e/transformed/jetified-xstream-1.4.10.jar) to field java.lang.reflect.Proxy.h
    WARNING: Please consider reporting this to the maintainers of com.thoughtworks.xstream.core.util.Fields
    WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
    WARNING: All illegal access operations will be denied in a future release
   */
  @Test
  @Throws(Exception::class)
  fun testFinalMethod() {
    val mock = PowerMockito.mock(Banana::class.java)
    PowerMockito.`when`(mock.isLike()).thenReturn(false)
    Assert.assertFalse(mock.isLike())
  }
}

Robolectric

主要用于Android上,比如可以对Activity、Service等进行测试,它通过实现一套JVM能运行的Android代码,从而做到脱离Android运行环境进行测试。

官方文档

配置

testImplementation "org.robolectric:robolectric:4.6.1"

通用配置

定义一个Robolectric的测试基类,可以更方便的在后期使用:”

//指定测试运行器为RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
//通过@Config指定sdk版本等其他配置,注意:如果sdk版本不指定,会使用当前gradle配置的版本,但是如果出现异常,可以考虑降一个版本试试
@Config(sdk = [Build.VERSION_CODES.R])
open class BaseRobolectricTest {
  open lateinit var mainActivity: MainActivity
  private lateinit var scenario: ActivityScenario<MainActivity>
  @Before
  open fun setUp() {
    //输出日志,通过以下方式可以输出Log日志信息
    ShadowLog.stream = System.out
    //方式一
    //mainActivity = Robolectric.setupActivity(MainActivity::class.java)
    //方式二
    scenario = ActivityScenario.launch(MainActivity::class.java)
    scenario.onActivity {
      mainActivity = it
    }
  }
}

示例

创建Activity

class RobolectricTest : BaseRobolectricTest() {
  @Before
  override fun setUp() {
    super.setUp()
  }
  @Test
  fun testMainActivity() {
    assertNotNull(mainActivity)
  }
}

验证Activity跳转

//功能代码:MainActivity.kt
btnLogin.setOnClickListener{
  startActivity(Intent(this, LoginActivity::class.java))
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  private lateinit var btnLogin: Button
  @Before
  override fun setUp() {
    super.setUp()
    btnLogin = mainActivity.findViewById(R.id.btn_login)
  }
  @Test
  fun testJump() {
    //触发按钮点击,会真实执行MainActivity中的代码
    //Robolectric会在本地jvm中运行,不会看到相关的界面,并非在模拟器或手机上运行,
    btnLogin.performClick()
    //获取对应的Shadow类
    val shadowActivity = Shadows.shadowOf(mainActivity)
    //借助Shadow类获取启动下一Activity的Intent
    val nextIntent: Intent? = shadowActivity.nextStartedActivity
    //校验Intent的正确性
    nextIntent?.component?.let {
      Assert.assertEquals(it.className, LoginActivity::class.java.name)
    } ?: Log.d("testJump", "未启动LoginActivity")
  }
}

验证Toast

//功能代码:MainActivity.kt
btnToast.setOnClickListener{
  Toast.makeText(this,"coding for fun",Toast.LENGTH_LONG).show()
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  private lateinit var btnToast: Button
  @Before
  override fun setUp() {
    super.setUp()
    btnToast = mainActivity.findViewById(R.id.btn_toast)
  }
  @Test
  fun testToast() {
    var toast = ShadowToast.getLatestToast()
    //判断Toast尚未弹出
    Assert.assertNull(toast)
    btnToast.performClick()
    toast = ShadowToast.getLatestToast()
    //判断Toast已经弹出
    Assert.assertNotNull(toast)
    //获取Shadow类进行验证
    Assert.assertEquals("coding for fun", ShadowToast.getTextOfLatestToast())
  }
}

验证Dialog

//功能代码:MainActivity.kt
btnDialog.setOnClickListener{
  val alertDialog = AlertDialog.Builder(this)
    .setMessage("dialog show")
    .setTitle("tips")
    .create()
  alertDialog.show()
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  private lateinit var btnDialog: Button
  @Before
  override fun setUp() {
    super.setUp()
    btnDialog = mainActivity.findViewById(R.id.btn_dialog)
  }
  @Test
  fun testDialog() {
    var dialog = ShadowAlertDialog.getLatestAlertDialog()
    //判断Dialog尚未弹出
    Assert.assertNull(dialog)
    btnDialog.performClick()
    //注意:由于ShadowAlertDialog.getLatestAlertDialog()返回的是android.app.AlertDialog,MainActivity也需要对应上
    dialog = ShadowAlertDialog.getLatestAlertDialog()
    //判断Dialog已经弹出
    Assert.assertNotNull(dialog)
    val shadowAlertDialog = Shadows.shadowOf(dialog)
    Assert.assertEquals("dialog show", shadowAlertDialog.message)
  }
}

验证UI组件状态

//功能代码:MainActivity.kt
btnCheckBox.setOnClickListener{
  checkBox.isChecked = !checkBox.isChecked
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  private lateinit var btnCheckBox: Button
  private lateinit var checkBox: CheckBox
  @Before
  override fun setUp() {
    super.setUp()
    btnCheckBox = mainActivity.findViewById(R.id.btn_check_box)
    checkBox = mainActivity.findViewById(R.id.checkBox)
  }
  @Test
  fun testCheckBox() {
    // 验证CheckBox初始状态
    Assert.assertFalse(checkBox.isChecked)
    // 点击按钮反转CheckBox状态
    btnCheckBox.performClick()
    // 验证状态是否正确
    Assert.assertTrue(checkBox.isChecked)
  }
}

验证Fragment

class RobolectricTest : BaseRobolectricTest() {
  @Before
  override fun setUp() {
    super.setUp()
  }
  @Test
  fun testFragment() {
    val scenario = FragmentScenario.launch(MyFragment::class.java)
    scenario.onFragment { fragment: MyFragment ->
      Assert.assertNotNull(fragment.view)
    }
  }
}

访问资源文件

class RobolectricTest : BaseRobolectricTest() {
  @Before
  override fun setUp() {
    super.setUp()
  }
  @Test
  fun testResource() {
    val application = RuntimeEnvironment.getApplication()
    val tips = application.getString(R.string.coding_life)
    Assert.assertEquals("Coding is my life!", tips)
  }
}

Activity生命周期

//功能代码
class MainActivity : AppCompatActivity() {
  private var lifecycle: String? = null
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycle = "onCreate"
  }
  override fun onDestroy() {
    super.onDestroy()
    lifecycle = "onDestroy"
  }
  fun getLifecycleState(): String? {
    return lifecycle
  }
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  @Before
  override fun setUp() {
    super.setUp()
  }
  @Test
  fun testLifecycle() {
    //创建Activity控制器
    val controller = Robolectric.buildActivity(MainActivity::class.java)
    val activity = controller.get()
    Assert.assertNull(activity.getLifecycleState())
    //调用Activity的performCreate方法
    controller.create()
    Assert.assertEquals("onCreate", activity.getLifecycleState())
    //调用Activity的performDestroy方法
    controller.destroy()
    Assert.assertEquals("onDestroy", activity.getLifecycleState())
  }
}

验证BroadcastReceiver

//功能代码
//MyReceiver在AndroidManifest文件中的action是com.androidtest
class MyReceiver : BroadcastReceiver() {
  companion object {
    val KEY_NAME: String = "unit_test"
  }
  override fun onReceive(context: Context, intent: Intent) {
    val editer = PreferenceManager.getDefaultSharedPreferences(context).edit()
    val name = intent.getStringExtra(KEY_NAME)
    editer.putString(KEY_NAME, name)
    editer.apply()
  }
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  private val ACTION = "com.androidtest"
  @Test
  fun testRegister() {
    val application = ShadowApplication.getInstance()
    val intent = Intent(ACTION)
    // 验证是否注册了相应的Receiver
    Assert.assertTrue(application.hasReceiverForIntent(intent))
  }
  @Test
  fun testReceive() {
    // 发送广播
    val intent = Intent(ACTION)
    intent.putExtra(MyReceiver.KEY_NAME, "myTest")
    val myReceiver = MyReceiver()
    myReceiver.onReceive(RuntimeEnvironment.getApplication(), intent)
    // 验证广播的处理逻辑是否正确
    val preferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.getApplication())
    val name = preferences.getString(MyReceiver.KEY_NAME, "")
    Assert.assertEquals("myTest", name)
  }
}

验证Service

//功能代码
class MyService : Service() {
  private var lifecycle: String? = null
  override fun onBind(intent: Intent?): IBinder? {
    lifecycle = "onBind"
    return null
  }
  override fun onCreate() {
    super.onCreate()
    lifecycle = "onCreate"
  }
  fun getLifecycleState(): String? {
    return lifecycle
  }
}
//测试代码
class RobolectricTest : BaseRobolectricTest() {
  @Test
  fun testLifecycle() {
    //创建Service控制器
    val controller = Robolectric.buildService(MyService::class.java)
    val service = controller.get()
    Assert.assertNull(service.getLifecycleState())
    //调用Service的bind方法
    controller.bind()
    Assert.assertEquals("onBind", service.getLifecycleState())
    //调用Service的create方法
    controller.create()
    Assert.assertEquals("onCreate", service.getLifecycleState())
  }
}

Shadow的使用

Robolectric通过实现一套JVM能运行的Android代码,从而做到脱离Android运行环境进行测试。实际上使用的就是Shadow,比如之前例子中的ShadowActivity、ShadowLog、ShadowAlertDialog等。Shadow在实现的同时,帮我拓展了原本的Android代码,实现了许多便于测试的功能,比如例子中用到的 getNextStartedActivity、ShadowToast.getTextOfLatestToast()、ShadowAlertDialog.getLatestAlertDialog()。当然不止这么多,Robolectric提供了大量的Shadow方便我们的使用。

示例

原始Person

class Person(var name:String? = "", var sex: Int = 0, val age: Int = 11) {
  fun eat(food: String): String {
    return food
  }
}

创建Person的Shadow对象ShadowPerson,实现与原始类方法名一致的方法,Shadow方法需用@Implementation进行注解。

@Implements(Person::class)
class ShadowPerson {
  @get:Implementation
  val name: String
    get() = "unitTest"

  @get:Implementation
  val age: Int
    get() = 18

  @get:Implementation
  val sex: Int
    get() = 0
}

在Config注解中添加shadows参数,指定对应的Shadow

@Config(shadows = [ShadowPerson::class])
class RobolectricTest : BaseRobolectricTest() {
  @Test
  fun testShadowShadow() {
    val person = Person()
    //实际上调用的是ShadowPerson的方法
    Log.d("shadow test", person.name + "")
    Log.d("shadow test", person.age.toString() + "")
    Log.d("shadow test", person.sex.toString() + "")
    //获取Person对象对应的Shadow对象
    val shadowPerson = Shadow.extract<ShadowPerson>(person)
    Assert.assertEquals("unitTest", shadowPerson.name)
    Assert.assertEquals(18, shadowPerson.age.toLong())
    Assert.assertEquals(0, shadowPerson.sex.toLong())
  }
}

参考文章

添加新评论