如何在多个活动之间测试Android应用程序?

81
我们正在构建一个复杂的Android应用程序,由许多屏幕和工作流程组成,分布在许多活动中。我们的工作流程类似于您可能在银行ATM机上看到的内容,例如,有一个登录的Activity,转换到主菜单Activity,根据用户的选择可以转换到其他活动。
由于我们有如此多的工作流程,我们需要创建跨多个活动的自动化测试,以便我们可以从头到尾地测试工作流程。例如,使用ATM示例,我们希望输入有效的PIN码,验证发送到主菜单,选择提取现金,验证我们在提取现金屏幕上等等,并最终回到主菜单或“注销”。
我们已经玩弄了随Android一起提供的测试API(例如ActivityInstrumentationTestCase2)以及Positron,但两者都似乎无法测试超出单个Activity的范围,虽然我们可以在某些单元测试中找到这些工具的一些实用性,但它们不能满足我们测试跨多个活动的场景的需求。
我们欢迎xUnit框架、脚本、GUI记录器/播放等,并感谢任何建议。

2
从Android 4.1开始,现在有一个新的测试框架,可以跨越活动甚至整个系统进行测试:http://developer.android.com/tools/testing/testing_ui.html - Christopher Orr
1
Robotium也可以满足这个需求,而且只需要几行代码。 - Dori
14个回答

65

回答自己的悬赏问题有点尴尬,但这就是答案...

我在这个问题上进行了高低搜索,但几乎不敢相信没有任何已发布的答案。我已经非常接近目标了。我现在可以跨越活动运行测试,但我的实现似乎存在一些时间问题,导致测试结果并不总是可靠。这是我所知道的唯一一个成功跨越多个活动的示例。希望我提取和匿名化过程中没有引入错误。这是一个简单的测试,在登录活动中输入用户名和密码,然后观察在不同的“欢迎”活动中是否显示了正确的欢迎消息:

package com.mycompany;

import android.app.*;
import android.content.*;
import android.test.*;
import android.test.suitebuilder.annotation.*;
import android.util.*;
import android.view.*;
import android.widget.*;

import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsNull.*;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.*;
import static com.mycompany.R.id.*;

public class LoginTests extends InstrumentationTestCase {

   @MediumTest
   public void testAValidUserCanLogIn() {

      Instrumentation instrumentation = getInstrumentation();

      // Register we are interested in the authentication activiry...
      Instrumentation.ActivityMonitor monitor = instrumentation.addMonitor(AuthenticateActivity.class.getName(), null, false);

      // Start the authentication activity as the first activity...
      Intent intent = new Intent(Intent.ACTION_MAIN);
      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      intent.setClassName(instrumentation.getTargetContext(), AuthenticateActivity.class.getName());
      instrumentation.startActivitySync(intent);

      // Wait for it to start...
      Activity currentActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 5);
      assertThat(currentActivity, is(notNullValue()));

      // Type into the username field...
      View currentView = currentActivity.findViewById(username_field);
      assertThat(currentView, is(notNullValue()));
      assertThat(currentView, instanceOf(EditText.class));
      TouchUtils.clickView(this, currentView);
      instrumentation.sendStringSync("MyUsername");

      // Type into the password field...
      currentView = currentActivity.findViewById(password_field);
      assertThat(currentView, is(notNullValue()));
      assertThat(currentView, instanceOf(EditText.class));
      TouchUtils.clickView(this, currentView);
      instrumentation.sendStringSync("MyPassword");

      // Register we are interested in the welcome activity...
      // this has to be done before we do something that will send us to that
      // activity...
      instrumentation.removeMonitor(monitor);
      monitor = instrumentation.addMonitor(WelcomeActivity.class.getName(), null, false);

      // Click the login button...
      currentView = currentActivity.findViewById(login_button;
      assertThat(currentView, is(notNullValue()));
      assertThat(currentView, instanceOf(Button.class));
      TouchUtils.clickView(this, currentView);

      // Wait for the welcome page to start...
      currentActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 5);
      assertThat(currentActivity, is(notNullValue()));

      // Make sure we are logged in...
      currentView = currentActivity.findViewById(welcome_message);
      assertThat(currentView, is(notNullValue()));
      assertThat(currentView, instanceOf(TextView.class));
      assertThat(((TextView)currentView).getText().toString(), is("Welcome, MyUsername!"));
   }
}

这段代码显然不是很易读。实际上,我已经将它提取到一个具有英语式API的简单库中,因此我可以像这样轻松地说出它的作用:

type("myUsername").intoThe(username_field);
click(login_button);

我已经测试了大约4个活动,我满意这种方法的效果,尽管如我所说,偶尔会出现一个时间问题,我还没有完全弄清楚。我仍然有兴趣听取任何其他跨活动测试的方法。


3
在某些情况下,您可以尝试添加FlakyTest注释以便在由于时间问题导致测试失败时自动重复进行测试。这并不是一个真正的解决方案,但是是一个可行的解决方法。 - Carl Manaster
谢谢你写这篇文章!我一直在寻找类似于ActivityMonitors的测试功能,但就是找不到。 - Peter Ajtai
据我所知,你上面所做的一切都可以使用ActivityInstrumentationTestCase2完成。 - ericn
你有什么想法,在什么情况下,'getInstrumentation().waitForIdleSync();' 会进入无限循环?在运行处理器板的Android 4.4.2_r2中,我在执行CTS测试时遇到了这个问题。 - ArunJTS
我认为我的儿子 @pajato1 找到并解决了你的时间问题。他的修复解决了我的问题。这是他说的: “我刚看到 javadoc 中提到 Instrumentation.startActivitySync() 会阻塞直到新的 Activity 准备好,然后返回它,所以似乎 Monitor 是不必要的。删除它证明了这一点。我的理论是由于竞争条件,Monitor 导致 startActivitySync() 创建的 Activity 在某些情况下被重新启动。我确实花了一些时间阅读 Android 源代码,但没有发现竞争条件的原因。” - pajato0

22

看看 Robotium
'一个开源测试框架,旨在使 Android 应用程序的自动黑盒测试显着快于使用 Android 测试工具箱自带功能的测试。'

主页: http://www.robotium.org/
源代码: http://github.com/jayway/robotium

请注意,Robotium 项目是我所在公司维护的。


你好,有没有录制工具可以用呢?我查了很多网站,发现Testdroid可以录制脚本并运行。不幸的是,它不是免费软件,你知道有没有任何免费软件可以进行录制过程吗? - thndrkiss
@thndrkiss:我不知道有这样的工具。如果你在Robotium论坛上提出问题,你可能会得到更好的答案。 - Jonas Söderström
2
Robotium是一个救星。它将使您的测试编写变得非常容易(您基本上在用普通英语与其交流:点击这个,按返回按钮等)。您可以测试任何东西,但不需要了解微小的细节。它至少有两个主要优点:您可以测试没有源代码的应用程序,并且它依赖于UI,使其非常强大(您更改控制器/模型比更改视图要多得多...) - tiktak

8

你可以使用Robotium。它支持类似Selenium的黑盒测试,但是适用于Android。你可以在Robotium.org找到它。


1
上次我检查时,Robotium不能跨活动使用。现在已经修复了吗? - user77115
3
只要这些活动属于同一个进程,它就一直可以跨活动运行。 - Renas

4

我很惊讶没有人提到一些主要的自动化功能测试工具。与Robotium相比,这些工具不需要编写Java代码。

MonkeyTalk:由Gorilla Logic公司支持的开源工具。优点:提供录制和更高级别的脚本语言,易于非技术用户使用,并且跨平台(包括iOS)。考虑到这些好处作为要求,我们发现这是最好的解决方案。它还允许通过Javascript进行定制,超越了他们的脚本语言能力。

Calabash-Android: 一款开源的 Cucumber 风格特性测试工具。优点:可以使用 Gherkin 语言编写特性,这是一种商业可读、领域特定语言,让您描述软件的行为而不详细说明该行为如何实现。类似但并非完全相同的支持也适用于 iOS 中的 cucumber-ios。录制功能不太好,因为它们会产生二进制输出。
其他参考资料:

3
我为Android创建了一个录制和播放工具,并在GitHub上提供。它易于配置和使用,不需要编程,在真实设备上运行(不必进行root),并在播放测试时自动保存屏幕截图。

这看起来很有前途。对于那些不明白的人来说:这似乎是一个相当不错的解决方案,用于测试手势(轻敲、拖动和其他操作)。 - tiktak

3
首先,将'InstrumentationTestCase'作为基类改为使用'ActivityInstrumentationTestCase2'。我使用Robotium经常在多个活动之间进行测试。我发现必须将登录活动指定为通用类型(并作为构造函数的类参数)。
'ActivityInstrumentationTestCase2'构造函数忽略包参数并且不需要它。带有包的构造函数已被弃用。
从Javadocs中可以看到: “ActivityInstrumentationTestCase2(String pkg, Class activityClass) 此构造函数已弃用,请改用ActivityInstrumentationTestCase2(Class)”
使用推荐的基类允许框架处理某些样板文件,例如启动您的活动。如果需要,这通过调用'getActivity()'完成。

3

我发现这篇文章非常有用,只需要稍作修改即可。 首先,getInstrumentation().waitForIdleSync()可以解决SingleShot所提到的不稳定性问题, 另外,InstrumentationTestCase有一个lauchActivity函数可以替代启动活动的代码。


2

您可以这样做,避免flake等待时间不同步:

final Button btnLogin = (Button) getActivity().findViewById(R.id.button);
Instrumentation instrumentation = getInstrumentation();

// Register we are interested in the authentication activity...
Instrumentation.ActivityMonitor aMonitor = 
        instrumentation.addMonitor(mynextActivity.class.getName(), null, false);

getInstrumentation().runOnMainSync(new Runnable() {
         public void run() {
             btnLogin.performClick();
         }
     });

getInstrumentation().waitForIdleSync();

//check if we got at least one hit on the new activity
assertTrue(getInstrumentation().checkMonitorHit(aMonitor, 1)); 

1

我正在处理类似的事情,可能会采用这个问题的已接受答案的变体,但在寻找解决方案时,我确实发现了Calculuon (gitHub)


0

这个答案基于被接受的答案,但经过修改以解决时间问题,在我添加了大约半打测试后变得更加一致。@pajato1因解决时间问题而获得了荣誉,正如在被接受的答案评论中所引用的那样。

/**
 * Creates a test Activity for a given fully qualified test class name.
 *
 * @param fullyQualifiedClassName The fully qualified name of test activity class.
 *
 * @return The test activity object or null if it could not be located.
 */
protected AbstractTestActivity getTestActivity(final String fullyQualifiedClassName) {
    AbstractTestActivity result = null;

    // Register our interest in the given activity and start it.
    Log.d(TAG, String.format("Running test (%s) with main class: %s.", getName(), fullyQualifiedClassName));
    instrumentation = getInstrumentation();

    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setClassName(instrumentation.getTargetContext(), fullyQualifiedClassName);
    // Wait for the activity to finish starting
    Activity activity = instrumentation.startActivitySync(intent);

    // Perform basic sanity checks.
    assertTrue("The activity is null!  Aborting.", activity != null);
    String format = "The test activity is of the wrong type (%s).";
    assertTrue(String.format(format, activity.getClass().getName()), activity.getClass().getName().equals(fullyQualifiedClassName));
    result = (AbstractTestActivity) activity;

    return result;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接