赞
踩
目录
使用 Espresso Intent 单独测试 Activity
Unit Test即单元测试,单元测试是应用测试策略中的基本测试。通过针对代码创建和运行单元测试,您可以轻松验证各个单元的逻辑是否正确。在每次构建后运行单元测试可帮助您快速捕捉和修复由应用的代码更改导致的软件回归。
测试应用是应用开发过程中不可或缺的一部分。通过持续对应用运行测试,您可以在公开发布应用之前验证其正确性、功能行为和易用性。
测试还会为您提供以下优势:
为了测试 Android 应用,您通常会创建下面这些类型的自动化单元测试:
根据单元有没有外部依赖(如Android依赖、其他单元的依赖),将本地测试分为两类,首先看看没有依赖的情况:
- dependencies {
- //Required--JUnit 4 framework
- testImplementation 'junit:junit:4.12'
- }
事实上,AS已经帮我们创建好了测试代码存储目录。
app/src
├── androidTestjava (仪器化单元测试、UI测试)
├── main/java (业务代码)
└── test/java (本地单元测试)
可以自己手动在相应目录创建测试类,AS也提供了一种快捷方式:选择对应的类->将光标停留在类名上->按下ALT + ENTER->在弹出的弹窗中选择Create Test
Note: 勾选setUp/@Before会生成一个带@Before注解的setUp()空方法,tearDown/@After则会生成一个带@After的空方法。
- package com.example.hellounittest;
-
- import ...
-
- public class EmailValidatorTest {
-
- @Test
- public void isValidEmail() {
- assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
- }
- }
上面的写法已过时,如果您更愿意使用 junit.Assert 其他方法来比较预期结果与实际结果,也可以改用这些库,比如:
- package com.example.hellounittest;
-
- import ...
-
- public class EmailValidatorTest {
-
- @Test
- public void isValidEmail() {
- assertTrue(EmailValidator.isValidEmail("name@email.com"));
- }
- }
从结果可以清晰的看出,测试的方法为 EmailValidatorTest 类中的 isValidEmail()方法,测试状态为passed,耗时12毫秒。
修改一下前面的例子,传入一个非法的邮箱地址:
- @Test
- public void isValidEmail() {
- assertTrue(EmailValidator.isValidEmail("#name@email.com"));
- }
测试状态为failed,耗时19毫秒,同时也给出了详细的错误信息:在11行出现了断言错误。
Annotation | 描述 |
@Test public void method() | 定义所在方法为单元测试方法 |
@Test (expected = Exception.class) public void method() | 测试方法若没有抛出Annotation中的Exception类型(子类也可以)->失败 |
@Test(timeout=100) public void method() | 性能测试,如果方法耗时超过100毫秒->失败 |
@Before public void method()
| 这个方法在每个测试之前执行,用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用。 |
@After public void method() | 这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。 |
@BeforeClass public static void method() | 这个方法在所有测试开始之前执行一次,用于做一些耗时的初始化工作(如: 连接数据库),方法必须是static |
@AfterClass public static void method() | 这个方法在所有测试结束之后执行一次,用于清理数据(如: 断开数据连接),方法必须是static |
@Ignore或者@Ignore("太耗时") public void method() | 忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的 |
@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestClass{} | 使得该测试类中的所有测试方法都按照方法名的字母顺序执行,可以指定3个值,分别是DEFAULT、JVM、NAME_ASCENDING |
前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架Mockito就派上用场了。
添加依赖
- //Optional--Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
- testImplementation 'org.mockito:mockito-core:2.19.0'
一个Context.getString(int)的测试用例
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(MockitoJUnitRunner.class)
- public class MockUnitTest {
- private static final String FAKE_STRING = "AndroidUnitTest";
-
- @Mock
- private Context mMockContext;
-
- @Test
- public void readStringFromContext() {
- //模拟方法调用的返回值,隔离对Android系统的依赖
- when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING);
- Assert.assertEquals(mMockContext.getString(R.string.app_name), FAKE_STRING);
-
- when(mMockContext.getPackageName()).thenReturn("com.example.hellounittest");
- System.out.println(mMockContext.getPackageName());
- }
- }
通过模拟框架Mockito,指定调用context.getString(int)方法的返回值,达到了隔离依赖的目的。
在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。
仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。
- dependencies {
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.2'
- androidTestImplementation 'androidx.test:runner:1.3.0'
- androidTestImplementation 'androidx.test:rules:1.3.0'
-
- }
-
- android {
- ...
-
- defaultConfig {
- ...
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- }
- }
这里举一个操作SharedPreference的例子,这个例子需要访问Context类以及SharedPreference的具体实现,采用模拟隔离依赖的话代价会比较大,所以采用仪器化测试比较合适。
这是业务代码中操作SharedPreference的实现
- package com.example.hellounittest;
-
-
- import ...
-
-
- public class SharedPreferenceDao {
- private SharedPreferences mSharedPreferences;
-
- public SharedPreferenceDao(SharedPreferences sp) {
- this.mSharedPreferences = sp;
- }
-
- public SharedPreferenceDao(Context context) {
- this(context.getSharedPreferences("config", Context.MODE_PRIVATE));
- }
-
- public void put(String key, String value) {
- SharedPreferences.Editor editor = mSharedPreferences.edit();
- editor.putString(key, value);
- editor.apply();
- }
-
- public String get(String key) {
- return mSharedPreferences.getString(key, null);
- }
- }
创建仪器化测试类(app/src/androidTest/java)
- package com.example.hellounittest;
-
- import ...
-
- public class SharedPreferenceDaoTest {
- private static final String TEST_KEY = "instrumentedTest";
- private static final String TEST_VALUE = "仪器化测试";
-
- private SharedPreferenceDao mSpDao;
-
- @Before
- public void setUp() throws Exception {
- mSpDao = new SharedPreferenceDao(App.getContext());
- }
-
- @Test
- public void sharedPreferenceDaoWriteRead(){
- mSpDao.put(TEST_KEY, TEST_VALUE);
- Assert.assertEquals(TEST_VALUE, mSpDao.get(TEST_KEY));
- }
- }
运行方式和本地单元测试一样,这个过程会向连接的设备安装apk,测试结果将在Run窗口展示,如下图:
通过测试结果可以清晰看到状态passed,通过am instrument命令运行instrumented测试用例,该命令的一般格式:
am instrument [flags] <test_package>/<runner_class>
例如本例子中的实际执行命令:
- adb shell am instrument -w -r -e debug false -e class 'com.example.hellounittest.SharedPreferenceDaoTest' com.example.hellounittest.test/androidx.test.runner.AndroidJUnitRunner
-
- -w: 强制 am instrument 命令等待仪器化测试结束才结束自己(wait),保证命令行窗口在测试期间不关闭,方便查看测试过程的log
- -r: 以原始格式输出结果(raw format)
- -e: 以键值对的形式提供测试选项,例如 -e debug false
-
- 关于这个命令的更多信息请参考
- https://developer.android.com/studio/test/command-line?hl=zh-cn
这里可以看出,这个过程向模拟器安装了两个apk文件,分别是HelloUnitTest和com.example.hellounittest.test,instrumented测试相关的逻辑在com.example.hellounittest.test中。最新版中,google对仪器化测试进行了很大的优化。从命令上来看是直接通过am instrument命令运行instrumented测试用例,感受不到两个apk的安装过程,速度上有了大大的提升。如果业务逻辑足够复杂,那么仪器化测试仍然会非常耗时。如果你实在没法忍受instrumented test的耗时问题,业界也提供了一个现成的方案Robolectric。
主要是解决仪器化测试中耗时的缺陷,仪器化测试需要安装以及跑在Android系统上,也就是需要在Android虚拟机或真机上面,所以十分的耗时,基本上每次来来回回都需要几分钟时间。针对这类问题,业界其实已经有了一个现成的解决方案: Pivotal实验室推出的Robolectric,通过使用Robolectrict模拟Android系统核心库的Shadow Classes的方式,我们可以像写本地测试一样写这类测试,并且直接运行在工作环境的JVM上,十分方便。
- testImplementation 'org.robolectric:robolectric:4.2.1'
- android {
- ...
- testOptions {
- unitTests {
- includeAndroidResources = true
- }
- }
- }
模拟打开MainActivity,点击界面上面的Button,读取TextView的文本信息。
以下是MainAcitivity代码
- package com.example.hellounittest;
-
- import ...
-
- public class MainActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- final TextView textView = findViewById(R.id.textView);
- Button button = findViewById(R.id.button);
- button.setOnClickListener(v -> textView.setText("Robolectric Rocks!"));
- }
- }
测试类MainActivityTest(注意这个测试类是放在app/src/test/java/目录下的,因为这是一段本地测试代码)
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(RobolectricTestRunner.class)
- public class MainActivityTest {
- @Test
- public void clickingButtonTest() throws Exception {
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- Button button = activity.findViewById(R.id.button);
- TextView results = activity.findViewById(R.id.textView);
-
- //模拟点击按钮,调用OnClickListener#onClick
- button.performClick();
- Assert.assertEquals("Robolectric Rocks!", results.getText().toString());
- }
- }
运行本段代码,发现它很本地化测试一样,无需运行模拟器或真机,测试结果如下:
前面的小节介绍了通过仪器化测试的方式跑到真机上进行测试SharedPreferences操作,可能吐槽的点都在于耗时太长,现在通过Robolectric改写为本地测试来尝试减少一些耗时。
在实际的项目中,Application可能创建时可能会初始化一些其他的依赖库,不太方便单元测试,这里额外创建一个Application类,不需要在清单文件注册,直接写在本地测试目录即可。
public class RoboApp extends Application {}
在编写测试类的时候需要通过@Config(application = RoboApp.class)来配置Application,当需要传入Context的时候调用RuntimeEnvironment.application来获取:
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(RobolectricTestRunner.class)
- @Config(application = RoboApp.class)
- public class RobolectricSharedPreferenceDaoTest {
- public static final String TEST_KEY = "instrumentedTest";
- public static final String TEST_VALUE = "仪器化测试";
-
- SharedPreferenceDao spDao;
-
- @Before
- public void setUp() {
- //这里的Context采用RuntimeEnvironment.application来替代应用的Context
- spDao = new SharedPreferenceDao(RuntimeEnvironment.application);
- }
-
- @Test
- public void sharedPreferenceDaoWriteRead() {
- spDao.put(TEST_KEY, TEST_VALUE);
- Assert.assertEquals(TEST_VALUE, spDao.get(TEST_KEY));
- }
- }
运行上面的代码,会发现它想本地化测试一样跑起来了
通过界面测试,您可以确保应用满足其功能要求并达到较高的质量标准,从而更有可能成功地被用户采用。
界面测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,并验证其行为是否正常。不过,这种人工方法会非常耗时、繁琐且容易出错。一种更高效的方法是编写界面测试用例,以便以自动化方式执行用户操作。自动化方法可让您以可重复的方式快速可靠地运行测试。
测试单个应用内的用户交互有助于确保用户在与应用交互时不会遇到意外结果或体验不佳的情况。如果您需要验证应用的界面是否正常运行,应养成创建界面测试的习惯。
由 AndroidX Test 提供的 Espresso 测试框架提供了一些 API,用于编写界面测试以模拟单个目标应用内的用户交互。Espresso 测试可以在搭载 Android 2.3.3(API 级别 10)及更高版本的设备上运行。使用 Espresso 的主要好处在于,它可以自动同步测试操作与您正在测试的应用的界面。Espresso 会检测主线程何时处于空闲状态,以便可以在适当的时间运行测试命令,从而提高测试的可靠性。此外,借助该功能,您不必在测试代码中添加任何计时解决方法,如 Thread.sleep()。
Espresso 测试框架是基于插桩的 API,可与 AndroidJUnitRunner 测试运行程序一起使用。
配置 Espresso
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
在测试设备上关闭动画 - 如果让系统动画在测试设备上保持开启状态,可能会导致意外结果或导致测试失败。通过以下方式关闭动画:在“设置”中打开“开发者选项”,然后关闭以下所有选项:
Example
针对上文的MainActivity,我们再写一个Espresso框架的界面测试用例
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(AndroidJUnit4.class)
- public class MainActivityTest {
- @Rule
- public ActivityTestRule<MainActivity> activityRule
- = new ActivityTestRule<>(MainActivity.class);
-
- @Test
- public void clickingButtonTest() throws Exception {
- onView(withId(R.id.button)).perform(click());
- onView(withId(R.id.textView)).check(matches(withText("Robolectric Rocks!")));
- }
- }
通过使用 ActivityTestRule,测试框架会在带有 @Test 注释的每个测试方法运行之前以及带有 @Before 注释的所有方法运行之前启动被测 Activity。该框架将在测试完成并且带有 @After 注释的所有方法都运行后关闭该 Activity。
运行上面的测试用例,会在模拟器中启动app,并自动执行用例中的操作。最终得到如下测试结果。
执行操作
调用 onView() 方法并传入用于指定目标视图的视图匹配器,onView() 方法将返回一个 ViewInteraction 对象。调用 ViewInteraction.perform() 或 DataInteraction.perform() 方法,以模拟界面组件上的用户交互。您必须将一个或多个 ViewAction 对象作为参数传入。Espresso 将按照给定的顺序依次触发每项操作,并在主线程中执行这些操作。
ViewActions 类提供了用于指定常见操作的辅助程序方法的列表。您可以将这些方法用作方便的快捷方式,而不是创建和配置单个 ViewAction 对象。您可以指定以下操作:
如果目标视图位于 ScrollView 内,请先执行 ViewActions.scrollTo() 操作以在屏幕中显示该视图,然后再继续执行其他操作。如果已显示该视图,则 ViewActions.scrollTo() 操作将不起作用。
Espresso Intent 支持对应用发出的 intent 进行验证和打桩。使用 Espresso Intent,您可以通过以下方式单独测试应用、Activity 或服务:拦截传出 intent,对结果进行打桩,然后将其发送回被测组件。
如需测试 intent,您需要创建 IntentsTestRule 类(与 ActivityTestRule 类非常相似)的实例。IntentsTestRule 类会在每次测试前初始化 Espresso Intent,终止托管 Activity,并在每次测试后释放 Espresso Intent。
现在有两个Activity,FirstActivity可以输入一个字符串并点击按钮将字符串发送大SecondActivity,SecondActivity接收到字符串后,将字符串显示到界面上。
以下是FirstActivity代码:
- package com.example.hellounittest;
-
- import ...
-
- public class FirstActivity extends AppCompatActivity {
- public static final String EXTRA_MESSAGE = "com.example.myfirstapp.MESSAGE";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_first);
- }
-
- public void sendMessage(View view) {
- Intent intent = new Intent(this, SecondActivity.class);
- EditText editText = (EditText) findViewById(R.id.edit_message);
- String message = editText.getText().toString();
- intent.putExtra(EXTRA_MESSAGE, message);
- startActivity(intent);
- }
- }
以下是SecondActivity代码:
- package com.example.hellounittest;
-
- import ...
-
- public class SecondActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_second);
-
- Intent intent = getIntent();
- String message = intent.getStringExtra(FirstActivity.EXTRA_MESSAGE);
-
- TextView textView = findViewById(R.id.textView);
- textView.setText(message);
- }
-
- }
现在新建一个测试类SimpleIntentTest用于验证两个Activity直接传递的Intent是否正确,代码如下:
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(AndroidJUnit4.class)
- public class SimpleIntentTest {
- private static final String MESSAGE = "This is a test";
- private static final String PACKAGE_NAME = "com.example.hellounittest";
-
- /* Instantiate an IntentsTestRule object. */
- @Rule
- public IntentsTestRule<FirstActivity> intentsRule =
- new IntentsTestRule<>(FirstActivity.class);
-
- @Test
- public void verifyMessageSentToMessageActivity() {
- // 在EditText控件中输入一个字符串
- onView(withId(R.id.edit_message))
- .perform(typeText(MESSAGE), closeSoftKeyboard());
-
- // 点击按钮,发送一个消息到第二个Activity
- onView(withId(R.id.send_message)).perform(click());
-
- //验证SecondActivity接收到了一个包含正确报名和消息的intent
- intended(allOf(
- hasComponent(hasShortClassName(".SecondActivity")),
- toPackage(PACKAGE_NAME),
- hasExtra(FirstActivity.EXTRA_MESSAGE, MESSAGE)));
- }
- }
运行结果如下:
测试结果通过,耗时954ms
Espresso还可以测试WebView,可以参考官方示例。
通过涉及多个应用中的用户交互的界面测试,您可以验证当用户流跨入其他应用或系统界面时,您的应用是否能够正常运行。
UI Automator 测试框架来编写此类界面测试。通过 UI Automator API,您可以与设备上的可见元素进行交互,而不管焦点在哪个 Activity 上。您的测试可以使用方便的描述符(如显示在相应组件中的文本或其内容描述)来查找界面组件。
I Automator 测试框架是基于插桩的 API,可与 AndroidJUnitRunner 测试运行程序一起使用。
配置 UI Automator
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
Example
下面的例子演示在App和Launcher之间切换。在测试App功能之前会先回到Launcher,然后再从Launcher启动App,然后测试App首页的功能。测试代码如下:
- package com.example.hellounittest;
-
- import ...
-
- @RunWith(AndroidJUnit4.class)
- public class ChangeTextBehaviorTest {
- private static final String BASIC_SAMPLE_PACKAGE = "com.example.hellounittest";
- private static final int LAUNCH_TIMEOUT = 5000;
- private static final String STRING_TO_BE_TYPED = "Robolectric Rocks!";
-
- private UiDevice mDevice;
-
- @Before
- public void startMainActivityFromHomeScreen() {
- // 初始化UiDevice对象
- mDevice = UiDevice.getInstance(getInstrumentation());
-
- // 模拟按下home键回到桌面
- mDevice.pressHome();
-
- // 等待回到桌面
- final String launcherPackage = getLauncherPackageName();
- assertThat(launcherPackage, notNullValue());
- mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);
-
- // 启动自己的测试app
- Context context = getApplicationContext();
- final Intent intent = context.getPackageManager()
- .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances
- context.startActivity(intent);
-
- // 等待自己的测试app启动
- mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
- }
-
- @Test
- public void testChangeText_sameActivity() {
- // Type text and then press the button.
- mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "button"))
- .click();
-
- // Verify the test is displayed in the Ui
- UiObject2 changedText = mDevice
- .wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "textView")), 500);
- assertThat(changedText.getText(), is(equalTo(STRING_TO_BE_TYPED)));
- }
-
-
- private String getLauncherPackageName() {
- // Create launcher Intent
- final Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_HOME);
-
- // Use PackageManager to get the launcher package name
- PackageManager pm = getApplicationContext().getPackageManager();
- ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
- return resolveInfo.activityInfo.packageName;
- }
- }
运行上面的代码,会看到模拟器先回到桌面,然后再启动App,然会点击App首页的按钮,最后验证字符串是否改变。最后测试结果如下:
UiDevice 对象是您访问和操纵设备状态的主要方式。在测试中,您可以调用 UiDevice 方法检查各种属性的状态,如当前屏幕方向或显示屏尺寸。您的测试可以使用 UiDevice 对象执行设备级操作,如强制设备进行特定旋转、按方向键硬件按钮,以及按主屏幕和菜单按钮。
UI Automator 测试类的编写方式应与 JUnit 4 测试类相同。在测试类定义的开头添加 @RunWith(AndroidJUnit4.class) 注释。
在 UI Automator 测试类中实现以下编程模型:
针对Android的测试还有很多,比如针对Service的测试,比如针对ContentProvider的测试等。这些测试同上面的针对Activity的测试差异不大。详细的测试资料可参考官方资料:
https://developer.android.com/training/testing/unit-testing/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。