【问题标题】:How to prevent ActivityUnitTestCase from calling Application.onCreate?如何防止 ActivityUnitTestCase 调用 Application.onCreate?
【发布时间】:2011-02-11 13:29:58
【问题描述】:

我一定在这里遗漏了什么。 ActivityUnitTestCase 的 JavaDoc 建议这个测试用例测试一个与系统隔离的 Activity:

此类提供对单个活动的独立测试。测试中的 Activity 将在与系统基础架构的最小连接的情况下创建,您可以注入许多 Activity 依赖项的模拟或包装版本。

我假设这包括不实际启动应用程序。此外,它还公开了一个 setApplication 帮助器,可以用来注入模拟应用程序。

但是,任何ActivityUnitTestCase 我开始启动(实际)应用程序并调用其onCreate 方法。更准确地说,InstrumentationTestRunner 似乎正在这样做,甚至在我有机会在我的测试的 setUp 方法中使用 setApplication 之前就这样做了!很长一段时间我都没有注意到这一点,因为它似乎发生在测试套件启动期间甚至没有到达 Eclipse 断点的某个时间点,但是写入 onCreate 中的日志显示它实际上已被调用。

这完全超出了我的范围。当 Android 的测试运行程序实例化并执行实际应用程序时,为什么我要使用模拟应用程序对象?考虑到检测运行器在其自己的线程中运行,并且在这样做时会产生主应用程序线程,这将更加成问题。这意味着正在执行的测试和调用Application.onCreate 之间存在竞争条件。如果您在那里做了任何可能影响您的测试的事情,例如写入共享首选项文件,那么你就完蛋了,因为你的测试会随机失败。

我是否遗漏了什么,或者这只是测试框架中的严重疏忽?

更新 这似乎也会影响ApplicationTestCase。在我的测试用例开始之前,我可以在我的应用程序类'onCreate 中到达一个断点。我们在那里启动了一个即发即弃的 AsyncTask,它会随机失败,因为我没有机会模拟它(记住,那是在我的测试用例调用 setUp 之前)。这是我在 onCreate 这个晦涩的调用期间看到的堆栈跟踪:

Thread [<1> main] (Suspended (breakpoint at line 86 in QypeRadar))  
QypeRadar.onCreate() line: 86   
InstrumentationTestRunner(Instrumentation).callApplicationOnCreate(Application) line: 969   
ActivityThread.handleBindApplication(ActivityThread$AppBindData) line: 4244 
ActivityThread.access$3000(ActivityThread, ActivityThread$AppBindData) line: 125    
ActivityThread$H.handleMessage(Message) line: 2071  
ActivityThread$H(Handler).dispatchMessage(Message) line: 99 
Looper.loop() line: 123 
ActivityThread.main(String[]) line: 4627    
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
Method.invoke(Object, Object...) line: 521  
ZygoteInit$MethodAndArgsCaller.run() line: 868  
ZygoteInit.main(String[]) line: 626 
NativeStart.main(String[]) line: not available [native method]  

为什么测试运行者callApplicationOnCreate 即使the docs 明确指出:

在您的测试调用 createApplication() 之前,测试用例不会调用 onCreate()。这使您有机会在 onCreate() 之前设置或调整任何其他框架或测试逻辑。

这是一个彻头彻尾的谎言——它没有给我机会!

【问题讨论】:

  • 据我所知,只有两种方法可以解决此问题:1) 重写 InstrumentationTestRunner 以不调用 callApplicationOnCreate 并将其留给测试用例来决定是否需要或不是。不确定这会对其他测试产生什么影响。 2) 重写我们的 onCreate 方法,使其无副作用且具有幂等性(请记住,它将被调用两次,因为要使测试用例正常工作,您还必须调用 createApplication)。想法?
  • 所以,我创建了一个绕过callApplicationOnCreate 的自定义测试运行器,现在一切都按unit 测试的预期运行。对于功能测试,我仍然需要执行完整启动的默认测试运行程序,所以我现在有两个测试运行程序,这有点痛苦。
  • 有趣的文章。仅针对适当的测试用例类型绕过callApplicationOnCreate 是否足够简单,允许您为测试运行程序创建一个可以发送到上游的补丁?
  • 如果您使用构建系统或以其他方式通过命令行(即 ADB/Activity Manager)运行测试,那么这很简单:1)将您的单元测试保存在单独的包中您的功能测试; 2) 调用adb shell am instrument 时,将被测包与适当的测试运行器配对。当然,这意味着将有两个单独的测试运行,但我想这很公平。在 Eclipse/ADT 中事情变得更加复杂。在这里,您必须为每个测试用例定义要在 Eclipse 运行配置中使用的测试运行器。
  • 我们现在是 2014 年。这个问题有没有更好的解决方案?

标签: android unit-testing


【解决方案1】:

Roboguice 有同样的问题。检查它here

【讨论】:

  • 为有用的材料+1,我以前不知道attachBaseContext!但是,它没有帮助:测试运行程序仍然调用无参数构造函数,如果我删除它,我会得到一个异常:“InstantiationException -- newInstance failed: no ()”。如果我将 no-args ctor 留在其中,则仍会在原始 app 对象上调用 onCreate,无论我是否调用 setApplication,因此问题仍然存在。
  • 虽然这在理论上可以回答问题,it would be preferable 在这里包含答案的基本部分,并提供链接以供参考。
【解决方案2】:

我正在用 dagger 进行测试,所以这可能也是你的情况,因为你可能只想注入而不调用 Application.onCreate 中的任何内容,所以这个对我来说很好用(api17+):

private Context mContext;
private Application mApplication;

@Override
protected void setUp() throws Exception {
    super.setUp();

    mContext = new ContextWrapper(getInstrumentation().getTargetContext()) {
        @Override
        public Context getApplicationContext() {
            return mApplication;
        }
    };
    mApplication = new MyAppMock();
    mApplication.attachBaseContext(mContext); 

    setApplication(app);
}

public void testActivityCreated() {
    Intent intent = AboutActivity.createIntent(mContext);
    setActivityContext(mContext);
    startActivity(intent, null, null);
    assertNotNull(getActivity());
}

Application.attach(context)而不是Application.attachBaseContext()设置Application.mLoadedApk,否则会崩溃。

我已将所有内容放在一起并制作了演示应用程序,展示了如何使用 dagger 进行测试: https://github.com/vovkab/dagger-unit-test

它还展示了如何模拟您的应用程序,适用于任何 android 版本。

【讨论】:

  • 你能测试“dagger-unit-test”测试类吗?我自己尝试了这种方法但失败了,因为 Dagger 抱怨 TestModule 没有注册目标类——它看到目标类是匿名类 MyTestClass$2 而不是 InjectableAppMock (这是在模块“注入”属性中指定的)它的“模块”注释。
  • 不确定您在谈论什么示例代码,您提到的类名不匹配,此示例也不匹配 github 上托管的示例。在我托管在 github 上的示例应用程序中,我注入 MainActivity 类:@Module(injects = MainActivity.class)。是的,它工作正常,请在此处查看示例:github.com/vovkab/dagger-unit-test
  • 很抱歉没有尽快回复。我显然一定是输入了错误的代码——我刚回到我正在从事的项目,我确实意识到我错过了你的示例中对setActivityContext(mContext); 的调用。谢谢!
  • 我在运行版本 16 的设备上使用了基于您的 dagger-unit-test 的代码(例如 Build.VERSION_CODES.JELLY_BEAN)。对于这种情况,attachBaseContext() 覆盖使用反射在父类上调用“attach()”。但是,父“attach()”调用“attachBaseContext()”,所以我最终得到了无限递归。也许版本 16 应该包含在对 super.attachBaseContext() 的调用中?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-14
  • 2012-06-30
  • 2018-04-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多