Android Studio:仪器化测试中无法写入共享首选项

15

我正在编写一个测试用例来验证一个写入Shared Preferences的类。我正在使用Android Studio v1.5。

在旧版的Eclipse中,当使用AndroidTestCase时,会部署第二个apk文件到设备上,并且可以使用instrumentation context运行测试,因此可以在不修改主apk现有shared preferences文件的情况下使用instrumentation apk的shared preferences运行测试。

我花了整个早上的时间来尝试找出如何在Android Studio测试中获得非空上下文。显然,为Eclipse制作的单元测试与Android Studio测试框架不兼容,因为调用getContext()会返回null。我认为在这个问题中找到了答案:Get context of test project in Android junit test case

随着Android Studio旧版本没有完整的测试支持,事情已经发生了变化,所以很多答案都是hack方法。显然,现在你应该像这样编写测试,而不是扩展InstrumentationTestCaseAndroidTestCase

@RunWith(AndroidJUnit4.class)
public class MyTest {

    @Test
    public void testFoo(){
        Context instrumentationContext = InstrumentationRegistry.getContext();
        Context mainProjectContext = InstrumentationRegistry.getTargetContext();            
    }   
}

所以现在我有一个非空的Instrumentation上下文,并且getSharedPreferences方法返回一个看起来能够工作的实例,但实际上没有写入任何首选项文件。

如果我执行以下操作:

context = InstrumentationRegistry.getContext();      

然后SharedPreferences编辑器会正确写入并提交,不会抛出任何异常。仔细检查后,我发现该编辑器正在尝试写入此文件:

data/data/<package>.test/shared_prefs/PREFS_FILE_NAME.xml

但是该文件从未被创建或写入。

然而,使用以下内容:

context = InstrumentationRegistry.getTargetContext(); 

编辑器工作正常,偏好设置已写入此文件:

/data/data/<package>/shared_prefs/PREFS_FILE_NAME.xml
偏好设置以私密模式实例化:
SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
据我所知,在运行测试后,没有将测试 apk 上传到设备上。这可能解释了为什么文件未使用检测上下文进行编写。是否有可能这个上下文是一个静默失败的虚假上下文?
如果是这种情况,我应该如何获取一个真正的检测上下文以便在不改变主项目首选项的情况下编写首选项?

请注意,在Android Studio中仍然可以运行扩展InstrumentationTestCaseAndroidTestCase的旧测试。 - Code-Apprentice
你是如何运行你的测试的?你是使用Android Studio单元测试还是调用connectedCheck来运行它们的? - David Medenjak
你是将它作为 JUnit 测试(绿色和红色箭头的图标)运行还是作为 Android 测试(Android 图标)运行? - Code-Apprentice
@Code-Apprentice 我实例化了一个新的 File 并调用了 exists() 方法,它返回了 false。另一方面,使用主项目上下文会产生一个实际的文件,我甚至可以使用 FileReader 记录它的内容。 - Mister Smith
1
@MisterSmith 你如何为你的测试类提供SharedPreferences?根据你提供的信息,似乎只是写入默认的偏好设置(并自行获取它们)。 - David Medenjak
显示剩余16条评论
4个回答

5
原来使用测试环境上下文对象无法写入共享偏好设置,即使在eclipse中也是如此。以下为eclipse的等效测试:
import android.content.Context;
import android.content.SharedPreferences;
import android.test.InstrumentationTestCase;

public class SharedPrefsTest extends InstrumentationTestCase {

    public void test() throws Exception { 
        Context context = getInstrumentation().getContext();
        String fileName = "FILE_NAME";

        SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString("key", "value");
        editor.commit();

        SharedPreferences sharedPreferences2 = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
        assertEquals("value", sharedPreferences2.getString("key", null));
    }
}

我刚刚运行了它,但它还是失败了。偏好设置从未被写入。我认为在这种情况下禁止访问内部存储文件,因为调用Context.getFilesDir()会抛出InvocationTargetException异常,而在偏好设置文件上调用File.exists()也会抛出相同的异常(您可以通过调试器检查编辑器正在写入哪个文件,只需查找名为mFile的私有变量,该变量位于this.$0成员实例中)。
所以我之前的想法是错误的。我曾经认为我们过去在数据访问层测试中使用过Instrumentation Context,但实际上我们使用的是主要的Context (AndroidTestCase.getContext()),尽管我们为偏好设置和SQLite文件使用了不同的名称。这就是为什么单元测试没有修改常规应用程序文件的原因。

4

仪器将安装在您的应用程序旁边。应用程序将自行运行,因此读取和写入自己的SharedPreferences

奇怪的是,InstrumentationSharedPreferences被删除(或从未创建),但即使它们被创建,您也很难将它们传递到您的测试应用程序中。如上所述,仅调用context.getSharedPreferences();在您的应用程序内部仍将提供实际应用程序的首选项,永远不会是您的仪器的首选项。

您需要找到一种方法来向您的测试应用程序提供首选项。一个好的解决方案是像以下这样将首选项保留在您的Application中:

public class App extends Application {
    SharedPreferences mPreferences;
    public void onCreate() {
        mPreferences = getSharedPreferences(fileName, Context.MODE_PRIVATE);
    }

    // add public getter / setter
}

这样,您可以:

  1. 只要有上下文,就可以使用((App) context.getApplicationContext()).getPreferences()从单个源获取首选项。
  2. 在运行测试和启动任何活动之前自己设置首选项,以注入测试数据。

在测试设置中,调用以下内容以注入所需的任何首选项:

@Before
public void before() {
    ((App) InstrumentationRegistry.getTargetContext()).setPreferences(testPreferences);
}

请确保在每次测试后正确完成活动,以便每个测试都可以获得自己的依赖。

此外,强烈建议您考虑使用Mockito、其他框架或者简单地实现SharedPreferences接口来模拟SharedPreferences,因为这样可以大大简化与模型的交互验证。


我非常确定使用AndroidTestCase.getContext打开共享首选项会在仪器化apk私有文件下创建一个文件。我已经在至少4个Eclipse项目和测试中这样做了,测试通过,并且这是一个非常好的功能,因为主要的apk文件没有在设备上被修改。SQLite数据库也是同样的情况。但是,在Android Studio中,没有将仪器化apk文件部署到设备上,这让我想到这里有一个不同的机制在起作用。 - Mister Smith
我必须承认,我从未在Eclipse中运行过任何仪器测试。但我确信,一个仪器文件会被部署到手机上,你是如何检查它不存在的呢?通常,在已安装应用程序列表中,它会以其包名列出。 - David Medenjak
使用Android Studio时,不会将测试仪apk文件部署到设备上。需要澄清的是,这个问题与Android Studio无关,而是与您编写测试所使用的库有关。较新的Testing Support Library似乎具有一些不同的行为。您仍应该能够使用本机测试仪库复制在Eclipse中看到的完全相同的行为。 - Code-Apprentice
我错了。AndroidTestCase.getContext 不会返回 instrumentation context,而是主 context。但是在 Eclipse 中使用 InstrumentationTestCase.getInstrumentation().getContext 也不起作用,所以与 Android Studio 没有区别。 - Mister Smith
@DavidMedenjak:“仍将提供实际应用程序的首选项,而不是您的测试工具的首选项”,这句话让我产生了疑问,并使我走上了正确的轨道。赏金奖励当之无愧。 - Mister Smith

0
请注意,您仍然可以使用Android Studio运行旧式测试,这些测试扩展了InstrumentationTestCaseAndroidTestCaseActivityInstrumentationTestCase2
新的Testing Support Library提供了ActivityTestRule,用于对单个活动进行功能测试。我相信在尝试获取用于测试的Instrumentation和/或Context之前,您需要创建其中一个对象。Espresso文档中有使用此类的示例。

再次看一下这里。我告诉你AndroidTestCase.getContext返回null。无论如何,我不是在测试活动,也不想创建一个测试活动来运行我的测试。如果可能的话,我希望AndroidTestCase能像在eclipse中一样工作(我知道,我们在谈论Google,所以也许这太过分了)。 - Mister Smith
@MisterSmith 先生,恐怕我误解了您的问题。您能否发布一些用于测试的类代码? - Code-Apprentice
我现在手头没有代码,但测试非常简单:你可以写入共享首选项,然后读取以验证已写入的数据是否存在。 - Mister Smith
好的,我更新了问题以展示我如何在私有模式下打开首选项文件。除此之外,代码没有什么特别的。我通常使用“SharedPreferences.Editor”编写并提交。 - Mister Smith

0
如果你想在不创建Activity的情况下测试类,我发现使用Robolectric是最简单的方法。Robolectric提供了一个模拟上下文,它可以执行上下文的所有操作。事实上,这个上下文是我在单元测试中使用Robolectric的主要原因。

我有时间时会看一下它。 - Mister Smith
请慢慢来。Robolectric以前使用起来非常简单,但是随着Android的最新版本,它变得更加复杂了。 - Christine

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