GCX-HCI / tray

a SharedPreferences replacement for Android with multiprocess support
Apache License 2.0
2.29k stars 273 forks source link

Issue Registering Provider in Robolectric Tests #71

Open KioKrofovitch opened 8 years ago

KioKrofovitch commented 8 years ago

I'm trying to run Robolectric tests with code that uses Tray, but immediately get the following error for any classes that access AppPreferences:

java.lang.IllegalStateException: could not access stored data with uri content://com.example.tray/internal_preferences/com.example/version?backup=true. Is the provider registered in the manifest of your application?

Is there a suggested set up for unit testing classes that use Tray? I tried following the existing Tray library unit tests, but ran into issues since TrayContract is private scope.

passsy commented 8 years ago

I never tested it with Robolectric but it doesn't look like a big problem.

You have to make sure that context.getString(R.string.tray__authority) (here context is the Application context) returns the correct authority. I'm not sure if Robolectric works with the generated.xml files which could cause this error.

Please check if mocking the string resource works.

RuntimeEnvironment.application = spy(RuntimeEnvironment.application);
when(RuntimeEnvironment.application.getApplicationContext())
    .thenReturn(RuntimeEnvironment.application);

Resources spiedResources = spy(app.getResources());
when(app.getResources())
    .thenReturn(spiedResources);

when(spiedResources.getString(R.string.tray__authority))
    .thenReturn("my.authority.for.tray");
jannisveerkamp commented 8 years ago

As @passsy mentioned I guess Robolectric doesn't find a generated String for the authority.

I'm not an Robolectric expert, but you can provide a BuildConfig for it. Make sure you register the correct Provider for Robolectric too. Here is an example (which hopefully works 😄 )

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyAwesomeTest {

    @Before
    public void setUp() throws Exception {
        TrayContentProvider provider = new TrayContentProvider();
        provider.onCreate();
        ShadowContentResolver.registerProvider(BuildConfig.APPLICATION_ID + ".preferences", provider);
    }

    @Test
    public void myTotallyAwesomeTest() throws Exception {
        AppPreferences appPreferences = new AppPreferences(RuntimeEnvironment.application);
        appPreferences.put("key", "test");
        String test = appPreferences.getString("key");
        assertThat(test, equalTo("test"));
    }
}

Let me know if this works. Note that the parameters may vary in your case. This configuration should be valid for the Tray sample project.

KioKrofovitch commented 8 years ago

Thank you both so much for your help! I have now successfully moved on from the URI error, but still get an error during setUp when I attempt to access R.string.tray__authority.

android.content.res.Resources$NotFoundException: unknown resource 2131034186

I also get the same error if I skip the string resource mocking code and I attempt to run only the code suggested by @jannisveerkamp . I get the error when I call provider.onCreate() (which ultimately attempts to access R.string.tray__authority).

Its like Robolectric cannot even see this string resource at all.

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk=21)
public class TrayTest {

  @Before
  public void setUp() throws Exception {
    RuntimeEnvironment.application = spy(RuntimeEnvironment.application);
    when(RuntimeEnvironment.application.getApplicationContext())
        .thenReturn(RuntimeEnvironment.application);

    Resources spiedResources = spy(RuntimeEnvironment.application.getResources());
    when(RuntimeEnvironment.application.getResources())
        .thenReturn(spiedResources);

    when(spiedResources.getString(R.string.tray__authority))
        .thenReturn("com.example.tray");

    TrayContentProvider provider = new TrayContentProvider();
    provider.onCreate();
    ShadowContentResolver.registerProvider(BuildConfig.APPLICATION_ID + ".preferences", provider);
  }

  @Test
  public void myTotallyAwesomeTest() throws Exception {
    assertTrue(true);
    //AppPreferences appPreferences = new AppPreferences(RuntimeEnvironment.application);
    //appPreferences.put("key", "test");
    //String test = appPreferences.getString("key");
    //assertThat(test, equalTo("test"));
  }
}
KioKrofovitch commented 8 years ago

Turns out, Robolectric has a difficult time accessing generated resource values. At first I tried extending RobolectricGradleTestRunner class to add extra directories to the resource path, but this did not work. Ultimately, I had to create a shadow resource like this:

@Implements(Resources.class)
public class CustomShadowResource extends ShadowResources {
  @Override
  public CharSequence getText(int id) throws Resources.NotFoundException {
    if (id == R.string.tray__authority) return "com.example.tray";

    return super.getText(id);
  }
}

Then include this Shadow resource in my TrayTest class like this: @Config(constants = BuildConfig.class, sdk=21, shadows = CustomShadowResource.class)

While I have resolved the missing string resource issue, the provider registration issue has returned as soon as I attempt to create an AppPreferences object: java.lang.IllegalStateException: could not access stored data with uri content://com.example.tray/internal_preferences/com.example/version?backup=true. Is the provider registered in the manifest of your application?

passsy commented 8 years ago

I haven't had the time to look deeper into this. But I suggest to mock the Storage of Tray. Instead of TrayPreference you should work with AbstractTrayPreference and inject a Storage which doesn't require a ContenteProvider. You can use the MockTrayStorage. Of cause this doesn't allow you to test multiprocess stuff.

I note that mocking tray for tests isn't as simple as it should. #futureimprovement

nihk commented 6 years ago

@KioKrofovitch

@Implements(Resources.class)
public class CustomShadowResource extends ShadowResources {
  @Override
  public CharSequence getText(int id) throws Resources.NotFoundException {
    if (id == R.string.tray__authority) return "com.example.tray";

    return super.getText(id);
  }
}

I just tried this but it yields a compilation error: the class cannot resolve the override because there doesn't appear to be any getText(int) method in ShadowResources. I looked at the robolectric source code and don't see it there either. Am I missing something here? Maybe the API changed?

Edit: OK yes it appears the API changed between 3.0 (the version at the time of your post) and 3.1.

Edit2: I more or less copied the 3.0 implementation and integrated it into the code snippet above - below is working code in 3.8.

@Implements(Resources.class)
public class CustomShadowResources {

    @RealObject 
    private Resources realResources;

    public String getText(@StringRes int id) throws Resources.NotFoundException {
        if (id == R.string.tray__authority) return "com.example.tray";

        CharSequence text = Shadow.directlyOn(realResources, Resources.class).getText(id);
        return StringResources.processStringResources(text.toString());
    }
}