Java单元测试

JUnit单元测试

单元测试的目的

  1. 定位bug
  2. 保证最小单元内的实现方式是正确的
  3. 提高代码健壮性

基于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中常用的注解

  • @Test注解 用于声明需要测试的方法。
    @Test注解可以带两个参数:
  1. @Test (expected = Throwable.class)。测试方法若没有抛出指定的Throwable类型(子类也可以),测试不通过。
    /**
     * 如果测试该方法时产生一个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);
    }
  1. @Test(timeout=200) 。带该参数可以测试方法的性能,若方法执行超过200ms,测试不通过
    @Test(timeout = 200)
    public void testTimeOut() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  1. @Before。在每个测试之前执行,用于初始化类,读输入流等,在一个测试类中,每个@Test方法的执行都会触发一次调用。
    @After。在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。
	@Before
    public void before() {
        System.out.println("测试方法之前调用,用于初始化");
    }

    @Test
    public void test() {
        System.out.println("我是测试方法");
    }

    @After
    public void after() {
        System.out.println("测试方法之后调用,用于释放资源");
    }

Java单元测试_第1张图片
测试方法
一般使用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优点

  • 能够将待测试代码与其依赖进行隔离。
  • 让一些对Android存在一定依赖的测试代码,运行在本地JVM上。

Mockito的使用场景

  • 待测试代码对Android有较小的依赖。
  • 开发者希望待测试代码与其依赖隔离开。

引入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对象

  • mock对象:完全虚构的对象,除了自定义的行为外,无其他行为。
  • spy对象:部分虚构的对象,除了自定义行为外,其他行为参考真实对象的行为。

使用Mockito编写测试用例的大致流程:

  • 构造mock/spy对象
  • 定义对象的行为
  • 运行测试代码
  • 校验测试代码运行结果与预期结果

一. 构造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

引入 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());
}
  • Intent跳转
@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());
}
  • Dialog
@Test
public void testDialog(){
        //点击按钮,出现对话框
        dialogBtn.performClick();
        AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
        assertNotNull(latestAlertDialog);
}
  • Toast
@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相关的对象进行测试。

  • 框架提供的Shadow对象
@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);

} 
  • 自定义Shadow对象
    首先,创建原始对象Person
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());
    }
}

你可能感兴趣的:(测试)