A while back, we at Zoodles made a proposal for adding a mechanism to Robolectric to automatically check for proper coding practices relating to internationalization.
Since nobody tried to stop us, we built the feature and merged it into the Robolectric master repository. We’re documenting it here for people who’d like to use it in their own projects.
What StrictI18n mode does
Robolectric’s Stricti18n mode is inspired by the “Strictmode” feature introduced into Android with the Gingerbread release. Stricti18n will validate your usage of the Android API as tests run, and will throw a runtime exception (specifically, com.xtremelabs.robolectric.util.I18nException) when your code invokes select Android API calls that accept bare text as an argument. For instance, the TextView widget has the following overloads of #setText():
public final void setText (int resid)
public final void setText (int resid, TextView.BufferType type)
public final void setText (char[] text, int start, int len)
public final void setText (CharSequence text)
public void setText (CharSequence text, TextView.BufferType type)
The first two versions of #setText() are safe for i18n purposes, since they use integer resource IDs into the Android strings.xml resource file. The final three are unsafe, as they take bare strings, such as string literals hardcoded into Java source files.
During a unit test scenario, if the application invokes any of the latter three API calls, Robolectric will throw an I18nException. Thus, your unit tests can also be used as a validation mechanism that you are properly externalizing your string resources.
The StrictI18n mode also validates text attributes defined in XML files. For instance, the following snippet from an XML layout file is not i18n-safe:
<TextView
android:id="@+id/mail_inbox_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Your Inbox"/>
In this case, Robolectric would find that the android:text attribute is a bare string rather than a resource reference, and throw an I18nException as it inflates the layout. Robolectric performs this validation for layout, menu, and preference definition files.
How to Enable StrictI18n mode
By default, StrictI18n mode is off. You can enable or disable StrictI18n for your entire test suite, on a test case class basis, or on a test method basis.
Test Runner
The RobolectricTestRunner class has a switch to enable StrictIi18n mode globally. To enable i18n-strict mode globally, set the system property robolectric.strictI18n to true. This can be done via java system properties in either Ant or Maven.
Ant example:
<junit>
<!-- ... other junit options ... -->
<jvmarg value="-Drobolectric.strictI18n=true" />
</junit>
For Maven, I won’t pretend I know the answer, since I haven’t authored Maven POMs in quite a while, but this Stack Overflow article looks like it has the secret sauce.
If you are subclassing RobolectricTestRunner, you can also override the following method to implement your own policy to turn StrictI18n on and off:
protected boolean globalI18nStrictEnabled() {
return Boolean.valueOf(System.getProperty("robolectric.strictI18n"));
}
Where I work at Zoodles, we simply hammer it to “on” in our test runner, for all of our development:
public class KidModeTestRunner extends RobolectricTestRunner {
// ...
@Override
protected boolean globalI18nStrictEnabled() {
return true;
}
}
Annotations
Regardless of the global setting, you can use the following annotations to turn StrictI18n on and off for specific test cases:
com.xtremelabs.robolectric.annotation.EnableStrictI18n
com.xtremelabs.robolectric.annotation.DisableStrictI18n
This example enables StrictI18n for an entire test case class:
@RunWith(RobolectricTestRunner.class)
@EnableStrictI18n
public class SeekBarPreferenceTest {
@Before
public void setup() {
// ...
}
@Test
public void testConstructor() {
// ...
}
@Test
public void testOnCreateDialogView() {
// ...
}
}
This example disables StrictI18n for one specific test method:
/**
* Show Link ID should display text link
*/
@Test
@DisableStrictI18n
public void testShowLinkIdTrue() throws Exception {
mView.setItemId( 3 );
mView.setShowLinkId( true );
mView.updateDisplay();
assertThat( mView.getLinkIdView().getVisibility(), equalTo( View.VISIBLE ) );
assertThat( mView.getLinkIdView().getText().toString(), equalTo("3") );
}
Annotations always override the global setting.
Developer Notes
Here’s some things we learned while developing this feature and using it against our application.
Formatted Strings
In our application code, we frequently came across the following type of use case:
Toast.makeText( this, getString( R.string.native_game_blocked, app ),
Toast.LENGTH_LONG ).show();
We’re generating a formatted string based upon a resource:
<string name="native_game_blocked">Blocked app \"%1$s\". To unblock, visit Apps in Parent Dashboard.</string>
and substituting in a piece of domain data (which is known to be I18n-safe).
We found ourselves disabling StrictI18n on a few too many tests due to these legitimate use cases of the Android APIs which take bare string arguments. In our app, this problem cropped up most often with the following UI elements:
- Alert Dialogs (specifically with
AlertDialog.Builder)
Button
CheckBox
TextView
Toast
To solve this problem, we created a package containing subclasses of these common Android UI classes. They provide I18n-safe alternatives to the bare-string methods that wrap the string formatting. Wherever we have need for this kind of specialized formatting, we use our custom classes in the XML layouts.
To prevent the StrictI18n mode from firing on these wrapper methods at test time, we created shadows for them that call the shadow implementations of the offending Android API methods.
If there is demand for these special I18n-safe classes, we’d consider making them available on github.
Caveats
There are some other caveats. For instance, if your test coverage is poor things can slip through. If that’s the case, you should work on the root issue, which is the lack of test coverage. It’s also possible that if Robolectric has not implemented a shadow for the API in question, your app will get a free pass. StrictI18n mode cannot detect calls to Android methods with no shadow implementation (the next section will make the reason for this limitation clear).
Developing Shadows with StrictI18n mode
If you are writing shadows, you can indicate that a given API call is not I18n-safe by passing the i18nSafe parameter to the @Implementation annotation, like this example from ShadowTextView:
@Implementation(i18nSafe=false)
public void setText(CharSequence text) {
if (text == null) {
text = "";
}
this.text = text;
}
Robolectric will then intercept all calls to TextView#setText and throw exceptions if StrictI18n mode is enabled.