自定义 Shadow 对象在 Robolectric 中到底是如何工作的?

发布于 2024-12-02 13:48:27 字数 127 浏览 0 评论 0原文

如果我为我的 Activity 编写一个自定义 Shadow,并将其注册到 RobolectricTestRunner,那么框架是否会在 Activity 启动时用我的自定义 Shadow 拦截该 Activity?

谢谢。

If I write a custom Shadow for my Activity, and registering it with RobolectricTestRunner, will the framework intercept the Activity with my custom Shadow whenever it's started?

Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

天涯沦落人 2024-12-09 13:48:27

简短的回答是否定的。

Robolectric 对拦截和检测的类别有选择性。在撰写本文时,要检测的唯一类必须具有与以下选择器之一匹配的完全限定类名:

android.* 
com.google.android.maps.* 
org.apache.http.impl.client.DefaultRequestDirector

Robolectric 存在的全部原因是 Android SDK jar 中提供的类在 JVM 中调用时会抛出异常(即不在模拟器或设备上)。您的应用程序的 Activity 具有非“敌对”源(调用方法或构造函数时它可能不会引发异常)。 Robolectric 的预期目的是允许您测试应用程序的代码,否则由于 SDK 的编写方式而无法实现这一点。创建 Robolectric 的其他一些原因是:

  • SDK 并不总是具有允许您查询应用程序代码操作的 Android 对象状态的方法。可以写入阴影来提供对此状态的访问。
  • Android SDK 中的许多类和方法都是最终的和/或私有的或受保护的,因此很难创建应用程序代码所需的依赖项,否则这些依赖项将可供应用程序代码使用。

显然可以更改代码来隐藏任何类。过去有人讨论过将阴影功能提取到独立库中,以帮助使用其他一些对测试不利的 API 编写测试。

为什么要跟踪您的 Activity?

The short answer is no.

Robolectric is selective about what classes it intercepts and instruments. At the time of this writing, the only classes that will be instrumented must have a fully qualified classname match one of these selectors:

android.* 
com.google.android.maps.* 
org.apache.http.impl.client.DefaultRequestDirector

The whole reason for Robolectric's existence is that the classes provided in the Android SDK jar throw exceptions when invoked in a JVM (i.e. not on an emulator or device). Your application's Activity has source that is not 'hostile' (it probably does not throw exceptions when the methods or constructors are invoked). Robolectric's intended purpose is to allow you to put your application's code under test, which would otherwise not be possible due to the way the SDK is written. Some of the other reasons why Robolectric was created were:

  • The SDK does not always have methods that would allow you to query the state of the Android objects manipulated by your application's code. Shadows can be written to provide access to this state.
  • Many of the classes and methods in the Android SDK are final and/or private or protected, making it difficult to create the dependencies needed by your application code that would otherwise be available to your application code.

The code could clearly be changed to shadow any class. There has been talk in the past about extracting the shadowing features into a standalone library, to assist writing tests using some other test-hostile api.

Why do you want to shadow your Activity?

擦肩而过的背影 2024-12-09 13:48:27

Robolectric 2 对此有了显着的改变。您可以在配置中指定自定义阴影,而不是编写自己的阴影测试运行者。

例如:

@Config(shadows = {ShadowAudioManager.class, ShadowContextWrapper.class})

This has significantly changed with Robolectric 2. You can specify custom shadows in the configuration instead of writing your own TestRunner.

For example:

@Config(shadows = {ShadowAudioManager.class, ShadowContextWrapper.class})
停顿的约定 2024-12-09 13:48:27

是的,如果您对 RobolectricTestRunner 进行子类化,请向构造函数添加自定义包并在 bindShadowClasses 方法中加载 Shadow 类。无需使用 android.* 包技巧。

(注意:这是在 robolectric-1.1 中)

RobolectricTestRunner#setupApplicationState 中提供了许多可以覆盖的钩子。

这是我对 RobolectricTestRunner 的实现。

import org.junit.runners.model.InitializationError;

import com.android.testFramework.shadows.ShadowLoggerConfig;
import com.xtremelabs.robolectric.Robolectric;
import com.xtremelabs.robolectric.RobolectricTestRunner;

public class RoboRunner extends RobolectricTestRunner {

public RoboRunner(Class<?> clazz) throws InitializationError {
    super(clazz);
    addClassOrPackageToInstrument("package.you're.creating.shadows.of");
}

@Override
protected void bindShadowClasses() {
    super.bindShadowClasses(); // as you can see below, you really don't need this
    Robolectric.bindShadowClass(ShadowClass.class);
}

您可以继承更多

方法(来自 RobolectricTestRunner.class),

/**
 * Override this method to bind your own shadow classes
 */
protected void bindShadowClasses() {
}

/**
 * Override this method to reset the state of static members before each test.
 */
protected void resetStaticState() {
}

   /**
 * Override this method if you want to provide your own implementation of Application.
 * <p/>
 * This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
 *
 * @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
 *         Application if not specified.
 */
protected Application createApplication() {
    return new ApplicationResolver(robolectricConfig).resolveApplication();
}

这是在 Robolectric TestRunner 中调用它们的位置:

 public void setupApplicationState(final RobolectricConfig robolectricConfig) {
    setupLogging();
    ResourceLoader resourceLoader = createResourceLoader(robolectricConfig);

    Robolectric.bindDefaultShadowClasses();
    bindShadowClasses();

    resourceLoader.setLayoutQualifierSearchPath();
    Robolectric.resetStaticState();
    resetStaticState();

    DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig

    Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
}

Yes, if you subclass the RobolectricTestRunner, add a custom package to the constructor and load your Shadow classes in the bindShadowClasses method. No need to use the android.* package trick.

(Note: this is with robolectric-1.1)

There are a number of hooks provided in the RobolectricTestRunner#setupApplicationState that you can override.

Here's my implementation of the RobolectricTestRunner.

import org.junit.runners.model.InitializationError;

import com.android.testFramework.shadows.ShadowLoggerConfig;
import com.xtremelabs.robolectric.Robolectric;
import com.xtremelabs.robolectric.RobolectricTestRunner;

public class RoboRunner extends RobolectricTestRunner {

public RoboRunner(Class<?> clazz) throws InitializationError {
    super(clazz);
    addClassOrPackageToInstrument("package.you're.creating.shadows.of");
}

@Override
protected void bindShadowClasses() {
    super.bindShadowClasses(); // as you can see below, you really don't need this
    Robolectric.bindShadowClass(ShadowClass.class);
}

}

More methods you can subclass (from RobolectricTestRunner.class)

/**
 * Override this method to bind your own shadow classes
 */
protected void bindShadowClasses() {
}

/**
 * Override this method to reset the state of static members before each test.
 */
protected void resetStaticState() {
}

   /**
 * Override this method if you want to provide your own implementation of Application.
 * <p/>
 * This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
 *
 * @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
 *         Application if not specified.
 */
protected Application createApplication() {
    return new ApplicationResolver(robolectricConfig).resolveApplication();
}

Here's where they're called in the Robolectric TestRunner:

 public void setupApplicationState(final RobolectricConfig robolectricConfig) {
    setupLogging();
    ResourceLoader resourceLoader = createResourceLoader(robolectricConfig);

    Robolectric.bindDefaultShadowClasses();
    bindShadowClasses();

    resourceLoader.setLayoutQualifierSearchPath();
    Robolectric.resetStaticState();
    resetStaticState();

    DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig

    Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
}
会发光的星星闪亮亮i 2024-12-09 13:48:27

作为更新,我已经能够创建自己的类的影子,只要在任何可能的加载程序作用于该类之前小心绑定影子类即可。因此,根据说明,我在 RoboRunner 中做了:

@Override protected void bindShadowClasses() {
    Robolectric.bindShadowClass(ShadowLog.class);
    Robolectric.bindShadowClass(ShadowFlashPlayerFinder.class);
}

我是否提到过我有点作弊?上面的原始答案(当然)是正确的。所以我将其用于我的真实课程:

package android.niftyco;

public class FlashPlayerFinder {
  .. . 

正如人们所期望的那样,我的模拟(影子)位于我的测试包后面:

package com.niftyco.android.test;

@Implements(FlashPlayerFinder.class)
public class ShadowFlashPlayerFinder {
    @RealObject private FlashPlayerFinder realFPF;

    public void __constructor(Context c) {
        //note the construction
    }

    @Implementation
    public boolean isFlashInstalled() {
        System.out.print("Let's pretend that Flash is installed\n");
        return(true);
    }
}

As an update, I have been able to create shadows of my own classes, as long as am careful to bind the shadow class before any possible loader acts on that class. So, per the instructions, in the RoboRunner I did:

@Override protected void bindShadowClasses() {
    Robolectric.bindShadowClass(ShadowLog.class);
    Robolectric.bindShadowClass(ShadowFlashPlayerFinder.class);
}

Did I mention that I'm cheating a bit? The original answer above is (of course) correct. So I use this for my real class:

package android.niftyco;

public class FlashPlayerFinder {
  .. . 

And my mock (shadow) is in back in my test package, as one might expect:

package com.niftyco.android.test;

@Implements(FlashPlayerFinder.class)
public class ShadowFlashPlayerFinder {
    @RealObject private FlashPlayerFinder realFPF;

    public void __constructor(Context c) {
        //note the construction
    }

    @Implementation
    public boolean isFlashInstalled() {
        System.out.print("Let's pretend that Flash is installed\n");
        return(true);
    }
}
明天过后 2024-12-09 13:48:27

可能会迟到,但从这里:org.robolectric.bytecode.Setup,您可能会找到有关检测哪些类的更多详细信息。

  public boolean shouldInstrument(ClassInfo classInfo) {
    if (classInfo.isInterface() || classInfo.isAnnotation() || classInfo.hasAnnotation(DoNotInstrument.class)) {
      return false;
    }

    // allow explicit control with @Instrument, mostly for tests
    return classInfo.hasAnnotation(Instrument.class) || isFromAndroidSdk(classInfo);
  }

  public boolean isFromAndroidSdk(ClassInfo classInfo) {
    String className = classInfo.getName();
    return className.startsWith("android.")
        || className.startsWith("libcore.")
        || className.startsWith("dalvik.")
        || className.startsWith("com.android.internal.")
        || className.startsWith("com.google.android.maps.")
        || className.startsWith("com.google.android.gms.")
        || className.startsWith("dalvik.system.")
        || className.startsWith("org.apache.http.impl.client.DefaultRequestDirector");
  }

Might be late, but from here: org.robolectric.bytecode.Setup, you might find further detail about what classes are instrumented.

  public boolean shouldInstrument(ClassInfo classInfo) {
    if (classInfo.isInterface() || classInfo.isAnnotation() || classInfo.hasAnnotation(DoNotInstrument.class)) {
      return false;
    }

    // allow explicit control with @Instrument, mostly for tests
    return classInfo.hasAnnotation(Instrument.class) || isFromAndroidSdk(classInfo);
  }

  public boolean isFromAndroidSdk(ClassInfo classInfo) {
    String className = classInfo.getName();
    return className.startsWith("android.")
        || className.startsWith("libcore.")
        || className.startsWith("dalvik.")
        || className.startsWith("com.android.internal.")
        || className.startsWith("com.google.android.maps.")
        || className.startsWith("com.google.android.gms.")
        || className.startsWith("dalvik.system.")
        || className.startsWith("org.apache.http.impl.client.DefaultRequestDirector");
  }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文