2

我在测试 Android Activity 时需要为接口使用不同的实现类,而不是在生产中运行时。我熟悉使用模拟对象,但问题是 Android 上的 Activity 是由操作系统而不是我实例化的,所以当我获得对 Activity 的引用时,它已经被实例化并且已经使用了实现类。就我而言,我正在谈论的类是 Login 类。对于我的自动化测试,我希望能够控制登录结果。这是我的例子:

class MainActivity extends Activity {

    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        setContentView(R.layout.main);
        LoginMgr aLoginMgr = new LoginMgrImpl();
        if (aLoginMgr.authenticated()) {
            //do something
        } else {
            //do something else
        }
    }
}


public interface LoginMgr {
    public boolean authenticated();
}

在图书馆的其他地方,我有一个实现类:

public class LoginMgrImpl implements LoginMgr {
    //authenticate
}

在自动化测试期间,我希望能够LoginMgrImpl用一个测试版本替换生产,该版本将返回我测试所需的布尔值。这是我需要帮助的地方,因为我看不到如何MainActivity使用不同的LoginMgrImpl. 如果这很重要,我正在使用 Eclipse。当我调用 getActivity() 时,ActivityInstrumentationTestCase2<MainActivity>测试类会为我创建。MainActivity它调用无参数构造函数,所以我没有机会在LoginMgrImpl那里改变。Eclipse 控制构建,所以我看不到MainActivity用不同的实现库构建的方法。

你们中的任何一个更有经验的人可以为我指明一个成功自动化测试的方向吗?我不能是唯一一个试图模拟 Activity 对象使用的一些测试类的人,但我花了几个小时试图在论坛中找到解决方案,但没有成功。帮助!


根据大家的反馈,我尝试了各种解决方案,并找到了两个我可以接受的解决方案。

解决方案 1:一个在标记答案中进行了解释,涉及在 Eclipse 中设置一个单独的“项目”,该项目具有指向原始 src、res、assets 文件夹以及AndroidManifest.xml. 然后,我可以修改项目链接版本的项目属性,以引用支持我的测试的不同库实现。这有效,但感觉很笨拙。


解决方案 2:为我的项目定义一个 Application 子类,该子类保留LoginMgr. 然后将从 Application 实例中MainActivity检索。LoginMgr在测试期间,我可以ActivityUnitTestCase<MainActivity>用来注入一个引用模拟 LoginMgr 的模拟应用程序。以下是代码片段:

主要应用:(确保AndroidManifest.xml包括<Application android:name="MainApplication" ...>

class MainApplication extends Application {

    private static LoginMgr sLoginMgr = new LoginMgrImpl();

    public LoginMgr getLoginMgr() {
        return sLoginMgr;
    }

}

MainActivity 必须修改为:

class MainActivity extends Activity {

    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        setContentView(R.layout.main);

        // 2 lines added
        MainApplication aApplication = (MainApplication)getApplication();
        LoginMgr aLoginMgr = aApplication.getLoginMgr();

        if (aLoginMgr.authenticated()) {
            //do something
        } else {
            //do something else
        }
    }
}

单元测试必须是这样的:

public class MainActivityTest extends ActivityUnitTestCase<MainActivity> {

  public MainActivityTest() {
    super(MainActivity.class);
  }

  public void testUseAlternateLoginMgr() {
    MainApplication aMockApplication = new MainApplication()
    {
      @Override
      public LoginMgr getLoginMgr() {
        return new LoginMgr() {
          public boolean authenticated() {              {
            return true;
          }
        };
      }
    };

    setApplication(aMockApplication);

    Intent aDependencyIntent = new Intent("android.intent.action.MAIN");
    Activity aMainActivity = startActivity(aDependencyIntent, null, null);
    // verify results
  }
}

我可能会使用模拟框架而不是手动编码的模拟,但这只是一个说明。

我认为解决方案2是更好的方法。谢谢@yorkw!

4

5 回答 5

1

当我需要在 Android 应用程序中模拟或存根某些对象时,我已将需要存根的对象的创建隔离到 Application 类中的一个简单的单一位置;然后只需手动评论或取消评论我想要的版本,具体取决于它是否是测试。这绝对不是自动化的,也不是很理想,但它确实有效。

例如,让您的 Application 类拥有 LoginMgr 的(静态)实例,并使其可以通过静态方法访问。您可以使用某种配置数据来告诉 Application 类使用哪个实现。将配置数据传递到应用程序的一种方法是通过清单中的节点,如此<meta-data>所述。您甚至可以通过声明一个特殊的“测试”活动来自动执行此操作,该活动包括该节点(而不是该节点是 的子节点),并用于从活动中获取它。然后,您的 Eclipse 启动配置将指定要启动此“测试”活动,而不是常规的“默认”活动。<meta-data><application>getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA)

于 2012-05-09T16:57:09.963 回答
1

测试我的应用程序的一种方法是使用 linux 中的符号链接来创建我的应用程序的“可测试”版本,该版本使用与生产中使用的库不同的库。为此,我在 Eclipse 中创建了两个项目,Application 和 ApplicationTestable。应用程序是真正的项目。ApplicationTestable 是一个虚拟项目,它具有指向与 Application 相同的 src、res 和 assets 目录的符号链接。我还添加了一个指向 AndroidManifest.xml 文件的符号链接。然后,我通过进入项目属性并选择库的测试版本而不是生产版本,将 ApplicationTestable 链接到库的测试版本。然后我进入 jUnit 测试所在的 ApplicationTest 项目的项目属性,并让它引用 ApplicationTestable 项目。在这种情况下,我的库的测试版本总是对登录进行身份验证,而库的生产版本需要有效的 ID 和密码,并会尝试针对真实服务器进行身份验证。我仍然对更优雅的解决方案持开放态度,但我会继续使用这个解决方案,直到有人提供更好的想法。

于 2012-05-10T14:53:31.237 回答
1

我已经实现了您的第一个解决方案,但它迫使我使用 AndroidUnitTestCase 类并且存在未解决的问题:ActionBar 类getActionBar() 中的 NPE 在设备/模拟器上工作正常,但在测试用例中返回 null

我的解决方案是使用这样的东西:

public class LoginHelper {
    private static LoginMgr _loginMgr ;

    public static LoginMgr get_LoginMgrImpl(){
        if(_loginMgr == null) _loginMgr = new LoginMgrImpl();
        return _loginMgr ;
    }

    public static void set_LoginMgrImpl(LoginMgr loginMgr){
        _loginMgr = loginMgr ;
    }
}

在单元测试构造函数中设置适当的接口模拟实现,仅此而已。

public class ApplicationTest extends ActivityInstrumentationTestCase2<TestActivity> {
    TestActivity _activity;


    public ApplicationTest() {
        super(TestActivity.class);
        LoginHelper.set_LoginMgrImpl(new MockLoginMgr());
    }


    public void test1(){
        assertTrue(getActivity() != null);
    }
}
于 2014-04-24T06:56:04.797 回答
0

您可能想研究使用依赖注入来解决此问题。我知道的两个用于 Android 的 DI 框架是RoboGuiceGoogle Guice。它们看起来非常相似,甚至可能以某种方式相关;但我没有进行足够的调查以了解差异或关系。

于 2012-05-09T21:23:39.210 回答
0

也许我遗漏了一些基本的东西,但我会将子类Activity化为MainActivity,然后实现两个构造函数:

public MainActivity() {
  this(new LoginMgrImpl());
}

public MainActivity(LoginMgr loginMgr) {
  this.loginMgr = loginMgr;
}

然后我会重命名LoginMgrAuthenticationService. 然后我将重命名LoginMgrImplBlahBlahBlahAuthenticationService,其中“BlahBlahBlah”描述了我如何实现身份验证。(InMemoryAuthenticationService??LdapAuthenticationService

于 2012-05-16T17:28:30.720 回答