纯java代码的单元测试 这里的纯java代码指的是不包含Android包中的代码,我们使用Junit写单元测试即可。
比如我们有一个方法是用来格式化数字,返回保留两位小数后的字符串,方法如下
1 2 3 public static String numberFormat (double number) { return String.format(Locale.getDefault(),"%.2f" ,number); }
那么我们的单元测试可以这么写,依赖一下junit测试框架testImplementation 'junit:junit:4.+'
1 2 3 4 5 6 7 8 9 import org.junit.Test;import static org.junit.Assert.*;public class ExampleUnitTest { @Test public void testNumberFormat () { assertEquals("0.23" ,Util.numberFormat(0.232323 )); } }
这里说明一下,我们在写单元测试的时候,经常会需要初始化一些数据,但我们又不想在每个测试方法中都调用一遍初始化的方法,这里测试框架给出了四个注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @BeforeClass public static void init () { }@Before public void setUp () { }@After public void tearDown () { }@AfterClass public static void destroy () { }
@BeforeClass:只会执行一次,修饰的方法必须是静态的
@AfterClass:只会执行一次,修饰的方法必须是静态的
@Before:每次调用测试方法时都会执行一次
@After:每次测试方法执行完成后都会执行一次
代码中包含Android代码 但是,假如我们的代码中不小心”混入”了一些调用Android包的功能,比如验证邮箱的有效性,代码可能是这样的
1 2 3 4 import android.util.Patterns;public static boolean isEmailAddress (String address) { return Patterns.EMAIL_ADDRESS.matcher(address).matches(); }
这里导入了 android.util包,如果使用junit的话,在单元测试代码中会报一个空指针异常
1 2 3 4 5 6 @Test public void testEmailAddress () { Assert.assertTrue(Util.isEmailAddress("gg@gg.com" )); Assert.assertTrue(Util.isEmailAddress("huangyuan@chunyu.me" )); Assert.assertFalse(Util.isEmailAddress("wwww" )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 java.lang .NullPointerException at com.huangyuanlove .tdd_demo .Util .isEmailAddress (Util.java :15 ) at com.huangyuanlove .tdd_demo .ExampleUnitTest .testEmailAddress (ExampleUnitTest.java :27 ) at sun.reflect .NativeMethodAccessorImpl .invoke0 (Native Method) at sun.reflect .NativeMethodAccessorImpl .invoke (NativeMethodAccessorImpl.java :62 ) at sun.reflect .DelegatingMethodAccessorImpl .invoke (DelegatingMethodAccessorImpl.java :43 ) at java.lang .reflect .Method .invoke (Method.java :498 ) at org.junit .runners .model .FrameworkMethod $1 .runReflectiveCall (FrameworkMethod.java :59 ) at org.junit .internal .runners .model .ReflectiveCallable .run (ReflectiveCallable.java :12 ) at org.junit .runners .model .FrameworkMethod .invokeExplosively (FrameworkMethod.java :56 ) at org.junit .internal .runners .statements .InvokeMethod .evaluate (InvokeMethod.java :17 ) at org.junit .runners .ParentRunner $3 .evaluate (ParentRunner.java :306 ) at org.junit .runners .BlockJUnit4ClassRunner $1 .evaluate (BlockJUnit4ClassRunner.java :100 ) at org.junit .runners .ParentRunner .runLeaf (ParentRunner.java :366 ) at org.junit .runners .BlockJUnit4ClassRunner .runChild (BlockJUnit4ClassRunner.java :103 ) at org.junit .runners .BlockJUnit4ClassRunner .runChild (BlockJUnit4ClassRunner.java :63 ) at org.junit .runners .ParentRunner $4 .run (ParentRunner.java :331 ) at org.junit .runners .ParentRunner $1 .schedule (ParentRunner.java :79 ) at org.junit .runners .ParentRunner .runChildren (ParentRunner.java :329 ) at org.junit .runners .ParentRunner .access $100 (ParentRunner.java :66 ) at org.junit .runners .ParentRunner $2 .evaluate (ParentRunner.java :293 ) at org.junit .runners .ParentRunner $3 .evaluate (ParentRunner.java :306 ) at org.junit .runners .ParentRunner .run (ParentRunner.java :413 ) at org.junit .runner .JUnitCore .run (JUnitCore.java :137 ) at com.intellij .junit4 .JUnit4IdeaTestRunner .startRunnerWithArgs (JUnit4IdeaTestRunner.java :68 ) at com.intellij .rt .junit .IdeaTestRunner $Repeater.startRunnerWithArgs (IdeaTestRunner.java :33 ) at com.intellij .rt .junit .JUnitStarter .prepareStreamsAndStart (JUnitStarter.java :230 ) at com.intellij .rt .junit .JUnitStarter .main (JUnitStarter.java :58 )
因为我们的junit是跑在jvm上的,默认并没有加载android的包,这时候我们可以使用Robolectric 这个三方包来做测试。在gradle中添加一下依赖testImplementation 'org.robolectric:robolectric:4.2'
,别问为啥不用4.4,因为我还没整明白,Shadows方法不能用
在我们的单元测试类上加下注解,如下
1 2 3 4 5 6 7 8 9 @RunWith(RobolectricTestRunner.class) public class UtilTestWithRobolectric { @Test public void testEmailAddress () { Assert.assertTrue(Util.isEmailAddress("gg@gg.com" )); Assert.assertTrue(Util.isEmailAddress("huangyuan@chunyu.me" )); Assert.assertFalse(Util.isEmailAddress("wwww" )); } }
除了这个,我们还可以使用Robolectric来测试一些页面行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import androidx.test.core.app.ActivityScenario;import androidx.test.core.app.ApplicationProvider;import org.junit.Assert;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.robolectric.RobolectricTestRunner;import org.robolectric.Shadows;import org.robolectric.shadows.ShadowAlertDialog;import org.robolectric.shadows.ShadowToast;@RunWith(RobolectricTestRunner.class) public class UtilTestWithRobolectric { private ActivityScenario<MainActivity> scenario; @Before public void initScenario () { scenario = ActivityScenario.launch(MainActivity.class); } @Test public void testScenario () { Assert.assertNotNull(scenario); } @Test public void testEmailAddress () { Assert.assertTrue(Util.isEmailAddress("gg@gg.com" )); Assert.assertTrue(Util.isEmailAddress("huangyuan@chunyu.me" )); Assert.assertFalse(Util.isEmailAddress("wwww" )); } @Test public void testShowToast () { scenario.onActivity(new ActivityScenario .ActivityAction<MainActivity>() { @Override public void perform (MainActivity activity) { Toast toast = ShadowToast.getLatestToast(); Assert.assertNull(toast); activity.findViewById(R.id.show_toast).performClick(); toast = ShadowToast.getLatestToast(); Assert.assertNotNull(toast); ShadowToast shadowToast = Shadows.shadowOf(toast); Assert.assertEquals("show_toast" ,ShadowToast.getTextOfLatestToast()); Assert.assertEquals(Toast.LENGTH_SHORT,toast.getDuration()); } }); } @Test public void testShowDialog () { scenario.onActivity(new ActivityScenario .ActivityAction<MainActivity>() { @Override public void perform (MainActivity activity) { AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog(); Assert.assertNull(dialog); activity.findViewById(R.id.show_toast).performClick(); dialog = ShadowAlertDialog.getLatestAlertDialog(); Assert.assertNotNull(dialog); ShadowAlertDialog shadowDialog = Shadows.shadowOf(dialog); Assert.assertEquals("Hello!" , shadowDialog.getMessage()); } }); } @Test public void testGoToLogin () { scenario.onActivity(new ActivityScenario .ActivityAction<MainActivity>() { @Override public void perform (MainActivity activity) { activity.findViewById(R.id.login).performClick(); Intent expectedIntent = new Intent (activity, LoginActivity.class); Intent actual = Shadows.shadowOf(activity).getNextStartedActivity(); Assert.assertEquals(expectedIntent.getComponent(),actual.getComponent()); } }); } }
这里的ActivityScenario
是使用的 androidx.test.core.app包下的类,需要依赖testImplementation 'androidx.test:core:1.1.0'
当然在Robolectric
中也有对应的创建Activity的方法,不过在4.4版本中被废弃了,也推荐使用androidx.test包中创建Activity的方法。
Mock和Mockito 如何测试一个没有返回值的方法,一般是来看这个方法有没有得到调用。 假如我们有如下代码
1 2 3 4 5 6 7 8 public class LoginPresenter { public void setUserManager (UserManager userManager) { this .userManager = userManager; } public void login (String userName,String password) { userManager.performLogin(userName,password); } }
我们要验证mUserManager
的一些行为,首先要 mock UserManager 这个类,mock 这个类的方式是:Mockito.mock(UserManager.class);
mock 了UserManager
类之后,我们就可以开始测试了,验证一个对象的方法调用情况的方法是:Mockito.verify(objectToVerify).methodToVerify(arguments);
其中,objectToVerify
和methodToVerify
分别是你想要验证的对象和方法
1 2 3 4 5 6 7 8 @Test public void testLogin () { UserManager userManager = Mockito.mock(UserManager.class); LoginPresenter loginPresenter = new LoginPresenter (); loginPresenter.setUserManager(userManager); loginPresenter.login("a" , "b" ); Mockito.verify(userManager).performLogin("a" , "b" ); }
再假如我们在登录的时候需要先验证密码强度,但是我们测试的时候不关心这个验证逻辑,希望不管传入的密码是啥,都可以通过验证。我们就需要干预某些mock对象的方法行为
1 2 3 4 5 6 7 8 9 10 11 12 13 public void loginWithVerifyPassword (PasswordValidator passwordValidator, String userName,String password) { if (passwordValidator.verifyPassword(password)){ userManager.performLogin(userName,password); }else { System.out.println("密码不正确" ); } }public static class PasswordValidator { public boolean verifyPassword (String password) { return password != null && password.length() >5 ; } }
我们需要mock一下PasswordValidator这个类中verifyPassword的行为,这种指定 mock 对象的某个方法,让它返回特定值的写法如下:Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue);
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testLoginWithPasswordValidator () { LoginPresenter.PasswordValidator passwordValidator = Mockito.mock(LoginPresenter.PasswordValidator.class); Mockito.when(passwordValidator.verifyPassword(ArgumentMatchers.any())).thenReturn(true ); Assert.assertTrue(passwordValidator.verifyPassword("" )); Assert.assertTrue(passwordValidator.verifyPassword(null )); Assert.assertTrue(passwordValidator.verifyPassword("123" )); Assert.assertTrue(passwordValidator.verifyPassword("321321321" )); UserManager userManager = Mockito.mock(UserManager.class); LoginPresenter loginPresenter = new LoginPresenter (); loginPresenter.setUserManager(userManager); loginPresenter.loginWithVerifyPassword(passwordValidator, "a" , "b" ); Mockito.verify(userManager).performLogin("a" , "b" ); }
Androidx.test 最近在看Androidx包下的测试框架,对于我们来讲,单元测试不是很多,测试代码跑在模拟器或者真机上带来的时间消耗还是可以接受的。有时间撸一下对应的代码
以上