24

Android中有几种单元测试方法,测试我编写的自定义视图最好的方法是什么?

我目前正在测试它作为我在仪器测试用例中活动的一部分,但我宁愿只测试孤立的视图。

4

4 回答 4

20

缺少以视图为中心的 TestCase 实现的一个简单解决方案是在您的测试项目中创建一个包含您的视图的简单 Activity。这将允许您使用简单的 Activity 针对视图编写测试。活动测试的相关资料:

http://developer.android.com/reference/android/test/ActivityUnitTestCase.html

于 2011-03-23T21:13:19.277 回答
19

Well 单元测试是一种方法,通过该方法测试各个源代码单元以确定它们是否适合使用。因此,当您说要测试自定义视图时,可以检查自定义视图的各种方法,例如“onTouchEvent”、“onDown”、“onFling”、“onLongPress”、“onScroll”、“onShowPress”、“onSingleTapUp”、 “onDraw”和其他各种取决于您的业务逻辑。您可以提供模拟值并对其进行测试。我会建议两种方法来测试您的自定义视图。

1) Monkey 测试 Monkey 测试是由自动化测试工具执行的随机测试。猴子测试是一个单元测试,在运行时没有考虑特定的测试。在这种情况下,猴子是任何输入的生产者。例如,猴子测试可以在文本框中输入随机字符串以确保处理所有可能的用户输入,或者提供垃圾文件来检查对其数据有盲目信任的加载例程。这是一种黑盒测试技术,它可以在许多独特的条件下检查您的自定义视图,您会感到惊讶:)。

2)单元测试

2a) 使用 Robotium 单元测试框架

访问 Robotium.org 或http://code.google.com/p/robotium/并下载示例测试项目。Robotium 是一个非常易于使用的框架,可以轻松快速地测试 android 应用程序。我创建它是为了以最小的努力测试高级 android 应用程序。它与 ActivityInstrumentationTestCase2 结合使用。

2b) 使用 Android 测试框架

以下是参考链接:http: //developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.htmlhttp://developer.android.com/reference/android/test/ActivityUnitTestCase.html

对于初学者:http: //developer.android.com/guide/topics/testing/testing_android.html

根据一位用户的说法:除了轻松测试非平台相关逻辑之外,我还没有找到一种运行测试的聪明方法,到目前为止(至少对我而言)任何实际的平台逻辑测试都很麻烦。无论如何,这几乎不是微不足道的,因为我发现模拟器和我的实际设备之间的实现存在差异,并且我讨厌在我的设备上运行单元测试实现只是为了在之后删除应用程序。

我的策略是:尽量简洁,使逻辑经过深思熟虑,然后逐个测试实现(不太理想)。

此外,Stephen Ng 为 Android 项目解决方案的真正单元测试提供了很好的方法:https ://sites.google.com/site/androiddevtesting/

一位用户制作了一个截屏视频。

这是我制作的关于如何让单元测试工作的 ScreenCast。依赖于对 Context 或 Activity 对象的引用的简单单元测试和更复杂的单元测试。 http://www.gubatron.com/blog/2010/05/02/how-to-do-unit-testing-on-android-with-eclipse/

希望它可以帮助您在所有可能的条件下测试您的自定义视图 :)


评论(futlib)您的所有建议似乎都涉及测试 ACTIVITY,而我真的只想测试 VIEW。我可能想在其他活动中使用这个视图,所以用一个特定的来测试它对我来说没有多大意义。– 富特库

回答:要实现自定义视图,您通常会首先为框架在所有视图上调用的一些标准方法提供覆盖。例如自定义视图的“onDraw”、“onKeyDown(int, KeyEvent)”、“onKeyUp(int, KeyEvent)”、“onTrackballEvent(MotionEvent)”等。因此,当您想为您的自定义进行单元测试时,您必须测试这些方法,并为其提供模拟值,以便您可以在所有可能的情况下测试您的自定义视图。测试这些方法并不意味着您正在测试您的 ACTIVITY,但它意味着测试您在活动中的自定义视图(方法/函数)。此外,您最终必须将自定义视图放入 Activity 中,以便目标用户体验它。一旦彻底测试,

于 2011-03-18T06:30:57.963 回答
14

这是一个在许多情况下都可以正常工作的不同建议:假设您从布局文件中引用自定义视图,您可以使用 AndroidTestCase,膨胀视图,然后单独对其执行测试。这是一些示例代码:

my_custom_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<de.mypackage.MyCustomView ...

MyCustomView.java:

public class MyCustomView extends LinearLayout {

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setTitle(CharSequence title) {
        ((TextView) findViewById(R.id.mylayout_title_textView)).setText(title);
    }
...

MyCustomViewTest.java:

public class MyCustomViewTest extends AndroidTestCase {

    private MyCustomView customView;

    @SuppressLint("InflateParams")
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        customView = (MyCustomView) LayoutInflater.from(getContext())
            .inflate(R.layout.my_custom_layout, null);
    }

    public void testSetTitle_SomeValue_TextViewHasValue() {
        customView.setTitle("Some value");
        TextView titleTextView = (TextView) valueSelection.findViewById(R.id.mylayout_title_textView);
        assertEquals("Some value", titleTextView.getText().toString());
    }
...
于 2014-10-22T13:01:44.783 回答
0

我为我的自定义视图设置屏幕截图测试付出了很多努力。
以下是我如何做到这一点以及我在此过程中学到的一切。
这可能不是最方便的方法,但我还是把它放在这里。当然,现在在Jetpack Compose
中进行屏幕截图测试变得更容易了。

⚠ 注意#1

如果需要,您可以使用JUnit 4。我正在使用JUnit 5。因为 JUnit 5 从头开始​​构建在 Java 8 之上,所以它的插桩测试只能在运行 Android 8.0 (API 26) 或更新版本的设备上运行。较旧的手机/模拟器将完全跳过这些测试的执行,将它们标记为忽略

如果您想在 Android 上运行 JUnit 5 测试,请参阅此答案以了解如何设置它。

⚠ 注意#2

屏幕截图测试可能无法在其他设备上运行,即使它们具有相同的屏幕 DPI(它们可能根本无法在具有不同屏幕 DPI 的设备上运行)。例如,即使我在本地机器和 GitHub Actions 上使用相同的设备运行测试,它们也不会产生相同的结果(GitHub Actions 断言失败)。所以,我不得不在 GitHub Actions 上禁用它们。

如果您想禁用 GitHub Actions(或其他 CI)上的屏幕截图测试,请参阅此答案

⚠ 注意#3

如果您在检测测试中有资源(在androidTest源集中)并且您想引用它们的 id,您应该像这样使用它们(注意包名称后跟.test):

com.example.test.R.id.an_id

例如,如果您的包名称是为了在您的测试中访问src/androidTest/res/layout/my_layout.xmlmy.package.name中的布局文件,则使用.my.package.name.test.R.layout.my_layout

⚠ 注意#4

由于我们将测试屏幕截图保存在设备/模拟器的外部存储中,我们需要确保我们在清单中添加了WRITE_EXTERNAL_STORAGE权限,并在构建脚本中配置了adb 安装选项-g-r 。在 Marshmallow+ 上运行时,我们还需要在运行测试之前授予这些权限。-g用于在安装应用程序时授予权限(仅适用于 Marshmallow+),而-r用于允许重新安装应用程序。这些对应于adb shell pm install选项。请注意,这不适用于 Android Studio。

因此,在src/androidTest/目录中创建一个AndroidManifest.xml文件,并在其中添加以下内容:

<manifest package="my.package.name">
  <!-- For saving screenshots in tests -->
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                   tools:ignore="ScopedStorage"
                   tools:remove="android:maxSdkVersion"/>
  <application android:requestLegacyExternalStorage="true">
    <activity android:name=".MyActivityThatContainsTheView"/>
  </application>
</manifest>

并在您的库 Gradle 构建文件中添加 adb 安装选项:

android {
    // Note that adbOptions block is deprecated in Android Gradle Plugin 7.0.0;
    // replace adbOptions block with installation block
    adbOptions {
        installOptions("-g", "-r")
    }
}

⚠ 注意#5

我将参考截图(我想与当前截图进行比较的截图)保存在src/androidTest/assets目录中。因此,将该目录指定为库构建文件中的资产条目:

android {
    sourceSets {
        // This is Kotlin DSL; see https://stackoverflow.com/a/59920318 for groovy DSL
        get("debug").assets.srcDirs("src/androidTest/assets")
    }

⚠ 注意#6

要在运行测试时传递检测参数(如shouldSave在我的代码中),请执行以下操作:

  • 对于 Gradle 任务:
    • 从命令行运行任务:在任务名称后传递参数
      ./gradlew myTask -Pandroid.testInstrumentationRunnerArguments.shouldSave=true
    • 使用 Studio 运行任务:在运行配置中传递参数参数:字段
      -Pandroid.testInstrumentationRunnerArguments.shouldSave=true
  • 对于 Android Studio Android Instrumented Tests运行配置:从运行配置弹出窗口
    中 选择Edit Configurations...,然后选择您的运行配置,单击Instrumentation arguments:字段前面,然后添加名称值条目,如Name Value... shouldSave true

请参阅这篇文章这篇文章

⚠ 注意#7

第一次要运行屏幕截图测试时,以及每当您更新可能会改变其视觉效果的自定义视图时,您应该运行传递参数的测试trueshouldSave以便将新的屏幕截图保存在设备中(请参阅save下面代码中的上述方法的注释图片的位置),然后手动将新的屏幕截图复制到您的src/androidTest/assets/目录,以便它们成为新的参考。

⚠ 注意#8

确保为 Kotlin 使用-ktx版本的 androidx 库(如AndroidX Core 库)。
-ktx体包含有用的 Kotlin 扩展函数。例子:

implementation("androidx.core:core-ktx:1.6.0")

⚠ 注意#9

确保设备屏幕已打开并已解锁,以使活动进入恢复状态。

编码

这是我在src/androidTest/java/com/example/目录中的测试活动,它公开了我想将其屏幕截图作为属性的视图:

class MyActivityThatContainsTheView : AppCompatActivity() {

    lateinit var myView: MyView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(my.package.name.test.R.layout.my_layout_that_contains_the_view)
        myView = findViewById(my.package.name.test.R.id.my_view_id_in_the_layout_file)
    }
}

最后,这是我的测试以及我如何保存、加载和比较屏幕截图:

@DisabledIfBuildConfigValue(named = "CI", matches = "true")
class ScreenshotTestView {

    @JvmField
    @RegisterExtension
    val scenarioExtension = ActivityScenarioExtension.launch<MyActivityThatContainsTheView>()
    lateinit var scenario: ActivityScenario<MyActivityThatContainsTheView>
    // See ⚠ Caution #6 above in the post
    val shouldSave = InstrumentationRegistry.getArguments().getString("shouldSave", "false").toBoolean()
    val shouldAssert = InstrumentationRegistry.getArguments().getString("shouldAssert", "true").toBoolean()

    @BeforeEach fun setUp() {
        scenario = scenarioExtension.scenario
        scenario.moveToState(Lifecycle.State.RESUMED)
    }

    @Test fun test1() {
        val screenshotName = "screenshot-1"
        scenario.onActivity { activity ->
            val view = activity.myView
            view.drawToBitmap()
                    .saveIfNeeded(shouldSave, screenshotName)
                    .assertIfNeeded(shouldAssert, screenshotName)
        }
    }

    fun Bitmap.saveIfNeeded(shouldSave: Boolean, name: String): Bitmap {
        if (shouldSave) save(name)
        return this
    }
    
    fun Bitmap.assertIfNeeded(shouldCompare: Boolean, screenshotName: String) {
        if (shouldCompare) assert(screenshotName)
    }
    
    /**
     * The screenshots are saved in /Android/data/my.package.name.test/files/Pictures
     * on the external storage of the device.
     */
    private fun Bitmap.save(name: String) {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(path, "$name.png")
        file.outputStream().use { stream ->
            compress(Bitmap.CompressFormat.PNG, 100, stream)
        }
    }
    
    private fun Bitmap.assert(screenshotName: String) {
        val reference = loadReferenceScreenshot(screenshotName)
        // I'm using AssertJ library; you can simply use assertTrue(this.sameAs(reference))
        assertThat(this.sameAs(reference))
            .withFailMessage { "Screenshots are not the same: $screenshotName.png" }
            .isTrue()
    }
    
    private fun loadReferenceScreenshot(name: String): Bitmap {
        val context = InstrumentationRegistry.getInstrumentation().context
        val assets = context.resources.assets
        val reference = assets.open("$name.png").use { stream ->
            BitmapFactory.decodeStream(stream)
        }
        return reference
    }
}
于 2021-09-20T18:06:04.010 回答