单元测试的目的
基于Jvm的单元测试 —— JUnit
只在本地的Jvm虚拟机上运行,不依赖Android环境,可以最小化执行时间。
JUnit的使用
在module的build.gradle中的dependencies块中声明testImplementation ‘junit:junit:4.12’
apply plugin: '...'
...
android {...}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
...
}
然后在src目录下创建test目录,专用存放Jvm单元测试类。
JUnit中常用的注解
/**
* 如果测试该方法时产生一个ArithmeticException的异常,则表示测试通过
* 你可以改成int i = 1 / 1;运行时则会测试不通过-因为与你的期望的不符
*/
@Test(expected = ArithmeticException.class)
public void testDivisionWithException() {
int i = 1 / 0;
}
/**
* 运行时抛出一个IndexOutOfBoundsException异常才会测试通过
*/
@Test(expected = IndexOutOfBoundsException.class)
public void testEmptyList() {
new ArrayList<>().get(0);
}
@Test(timeout = 200)
public void testTimeOut() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Before
public void before() {
System.out.println("测试方法之前调用,用于初始化");
}
@Test
public void test() {
System.out.println("我是测试方法");
}
@After
public void after() {
System.out.println("测试方法之后调用,用于释放资源");
}
测试方法
一般使用org.junit.Assert包下的assertEquals(变量1, 变量2)来判断2个变量是否相同,相同则通过测试。或者使用assertThat(T, is())方法来判断返回值是否相同。
//可简写为 assertTrue(validMobile("13387654321"));
assertEquals(validMobile("13387654321"), true);
assertThat(validMobile("13387654321"), is(true));
Mockito的作用
Mockito优点
Mockito的使用场景
引入Mockito
dependencies {
testImplementation 'junit:junit:4.12' //必须引入
testImplementation 'org.mockito:mockito-core:1.10.19'
androidTestImplementation 'org.mockito:mockito-core:1.10.19'
}
Mockito对象分为mock对象与spy对象。
使用Mockito编写测试用例的大致流程:
一. 构造Mock对象
通过mock方法创建对象:
@Before
public void setUp() throws Exception {
mockedList = mock(ArrayList.class);
}
对于setUp()方法,由于添加了@Before注解,故该方法在所有@Test方法之前执行。在该方法中,将需要被构造的类传入mock方法中,能够创建出mock对象。
通过@Mock注解的方式创建对象
方法1:使用前调用iniMocks方法:
@Mock
private ArrayList mockedList;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
方法2:通过@RunWith注解:
@RunWith(MockitoJUnitRunner.class)
public class MockitoJUnitRunnerTest {
@Mock
AccountData accountData;
}
方法3:通过MockitoRule
public class MockitoRuleTest {
@Mock
AccountData accountData;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
}
二. 定义Mock对象的行为
when(mockedList.get(0)).thenReturn("first");
语法为当(when)调用什么时,返回(thenReturn)什么。对于上面的例子,即当调用mockedList.get(0)时,返回“first".
完整流程
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
@Mock
ArrayList mockedList; //创建mock对象
@Before
public void setUp(){
when(mockedList.get(0)).thenReturn("first"); //定义mock行为
//语法为当(when)调用什么时,返回(thenReturn)什么。对于上面的例子,即当调用mockedList.get(0)时,返回“first"。
}
@Test
public void testMockito(){
String res = (String)mockedList.get(0); //调用mock对象方法
assertEquals(res, "first"); //比较实际结果与预期
}
}
当Mock对象存在安卓依赖时的用法
@RunWith(MockitoJUnitRunner.class) //声明使用Mockito
public class UnitTestSample {
private static final String FAKE_STRING = "HELLO WORLD";
@Mock
Context mMockContext; //mock构造一个context对象
@Test
public void readStringFromContext_LocalizedString() {
// 定义该mock对象的行为,即通过id或者字符串
when(mMockContext.getString(R.string.hello_world)).thenReturn(FAKE_STRING);
// 将该mockContext传入某个类,安卓场景中一般都会传入context
ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);
// 运行测试代码,获取字符串
String result = myObjectUnderTest.getHelloWorldString();
// 校验实际结果与预期结果是否一致
assertThat(result, is(FAKE_STRING));
}
}
Verify语句的用法
当开发者希望验证mock对象的某个方法是否调用,以及调用的其他情况时,可以使用Mockito提供的verify方法。
verify(mock对象).方法(参数)
public class MockitoTest {
@Mock
ArrayList mockedList;
@Before
public void setUp(){
//when(mockedList.get(0)).thenReturn("first"); //即使注释掉定义定位的代码
}
@Test
public void testMockito(){
String res = (String)mockedList.get(0); //调用mock对象的get方法
verify(mockedList).get(0); //验证通过
verify(mockedList).get(1); //验证不通过
}
}
其他用法
verify(mockedList).get(0); //验证方法是否调用,且参数传入的是0
verify(mockedList, times(1)).get(0); //验证方法是否调用且只调用了1次
verify(mockedList, never()).get(0); //验证方法是否没有被调用过
verify(mockedList, atLeast(2)).get(0); //验证方法是否调用且调用2次以上
verify(mockedList, atMost(5)).get(0); //验证方法是否调用且最多调用5次
verify(mockedList).get(anyInt()); //验证方法是否调用,且传入的参数是任意整型数
Spy对象的使用
@RunWith(MockitoJUnitRunner.class)
public class AssertEquals {
@Test
public void testMockito(){
ArrayList integers = new ArrayList<>();
ArrayList spyList = spy(integers); //通过真实的对象创建spy对象
spyList.add(1); //真实对象添加了一个元素
System.out.println(spyList.get(0)); //查看其元素值,返回1
when(spyList.size()).thenReturn(100); //定义其行为,虚构其size为100
System.out.println(spyList.size()); //查看其size,返回100
when(spyList.get(0)).thenReturn(2); //定义其行为,虚构其第一个元素值
System.out.println(spyList.get(0)); //返回2,即虚构的行为会覆盖原有真实的行为
}
}
从上面的例子中可知,spy仅仅是对真实对象的行为进行部分虚构,虚构的部分可以覆盖原来的部分。
引入 Robolectric
testImplementation "org.robolectric:robolectric:4.2.1" //适用于test包下
androidTestImplementation "org.robolectric:robolectric:4.2.1" //适用于androidTest包下
通过注解配置TestRunner
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "build/intermediates/bundle_manifest/debug/processDebugManifest/bundle-manifest/AndroidManifest.xml")
public class RoboletricTest {
}
踩坑
上文中的Config需要手动指定AndroidManifest的路径
Activity的测试
@Test
public void testActivity() {
DemoActivity demoActivity = Robolectric.buildActivity(DemoActivity.class).setup().get();
assertNotNull(demoActivity);
assertEquals(demoActivity.getTitle(), demoActivity.getClass().getSimpleName());
}
@Test
public void testLifecycle() {
ActivityController activityController = Robolectric.buildActivity(SampleActivity.class).create().start();
Activity activity = activityController.get();
TextView textview = (TextView) activity.findViewById(R.id.tv_lifecycle_value);
assertEquals("onCreate",textview.getText().toString());
activityController.resume();
assertEquals("onResume", textview.getText().toString());
activityController.destroy();
assertEquals("onDestroy", textview.getText().toString());
}
@Test
public void testStartActivity() {
//按钮点击后跳转到下一个Activity
forwardBtn.performClick();
Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class);
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent, actualIntent);
}
@Test
public void testViewState(){
CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox);
Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse);
assertTrue(inverseBtn.isEnabled());
checkBox.setChecked(true);
//点击按钮,CheckBox反选
inverseBtn.performClick();
assertTrue(!checkBox.isChecked());
inverseBtn.performClick();
assertTrue(checkBox.isChecked());
}
@Test
public void testDialog(){
//点击按钮,出现对话框
dialogBtn.performClick();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(latestAlertDialog);
}
@Test
public void testToast(){
//点击按钮,出现吐司
toastBtn.performClick();
assertEquals(ShadowToast.getTextOfLatestToast(),"we love UT");
}
BroadcastReceiver的测试
广播接收代码
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences.Editor editor = context.getSharedPreferences(
"account", Context.MODE_PRIVATE).edit();
String name = intent.getStringExtra("EXTRA_USERNAME");
editor.putString("USERNAME", name);
editor.apply();
}
}
广播的测试点可以包含两个方面,一是应用程序是否注册了该广播,二是广播接受者的处理逻辑是否正确,关于逻辑是否正确,可以直接人为的触发onReceive()方法,验证执行后所影响到的数据。
测试代码
@Test
public void testBoradcast(){
ShadowApplication shadowApplication = ShadowApplication.getInstance();
String action = "com.geniusmart.loveut.login";
Intent intent = new Intent(action);
intent.putExtra("EXTRA_USERNAME", "geniusmart");
//测试是否注册广播接收者
assertTrue(shadowApplication.hasReceiverForIntent(intent));
//以下测试广播接受者的处理逻辑是否正确
MyReceiver myReceiver = new MyReceiver();
myReceiver.onReceive(RuntimeEnvironment.application,intent);
SharedPreferences preferences = shadowApplication.getSharedPreferences("account", Context.MODE_PRIVATE);
assertEquals( "geniusmart",preferences.getString("USERNAME", ""));
}
Service的测试
以IntentService为例,可以直接触发onHandleIntent()方法,用来验证Service启动后的逻辑是否正确。
public class SampleIntentService extends IntentService {
public SampleIntentService() {
super("SampleIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences(
"example", Context.MODE_PRIVATE).edit();
editor.putString("SAMPLE_DATA", "sample data");
editor.apply();
}
}
测试代码
@Test
public void addsDataToSharedPreference() {
Application application = RuntimeEnvironment.application;
RoboSharedPreferences preferences = (RoboSharedPreferences) application
.getSharedPreferences("example", Context.MODE_PRIVATE);
SampleIntentService registrationService = new SampleIntentService();
registrationService.onHandleIntent(new Intent());
assertEquals(preferences.getString("SAMPLE_DATA", ""), "sample data");
}
Shadow
Robolectric框架针对Android SDK中的对象,提供了很多影子对象(如Activity和ShadowActivity、TextView和ShadowTextView等),这些影子对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。
@Test
public void testDefaultShadow(){
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
//通过Shadows.shadowOf()可以获取很多Android对象的Shadow对象
ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
Bitmap bitmap = BitmapFactory.decodeFile("Path");
ShadowBitmap shadowBitmap = Shadows.shadowOf(bitmap);
//Shadow对象提供方便我们用于模拟业务场景进行测试的api
assertNull(shadowActivity.getNextStartedActivity());
assertNull(shadowApplication.getNextStartedActivity());
assertNotNull(shadowBitmap);
}
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
再创建Person的Shadow对象
@Implements(Person.class)
public class ShadowPerson {
@Implementation
public String getName() {
return "geniusmart";
}
}
最后,在测试用例中,ShadowPerson对象将自动代替原始对象,调用Shadow对象的数据和行为
@RunWith(CustomShadowTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowPerson.class})
public class ShadowTest {
/**
* 测试自定义的Shadow
*/
@Test
public void testCustomShadow(){
Person person = new Person("genius");
//getName()实际上调用的是ShadowPerson的方法
assertEquals("geniusmart", person.getName());
//获取Person对象对应的Shadow对象
ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);
assertEquals("geniusmart", shadowPerson.getName());
}
}