Skip to content

Instantly share code, notes, and snippets.

@JakeWharton
Created September 29, 2012 05:42
Show Gist options
  • Save JakeWharton/3803294 to your computer and use it in GitHub Desktop.
Save JakeWharton/3803294 to your computer and use it in GitHub Desktop.
Making Robolectric suck less... (barely)

You need to do this once in your base test runner:

ActionBarSherlock.registerImplementation(ActionBarSherlockRobolectric.class);

and your Robolectric should have this patch:

https://github.com/square/robolectric/commit/fac24e5ef6d5830e6e67ad27aff2f8f57cedb5a1
// Copyright 2012 Square, Inc.
package com.squareup.test;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import com.actionbarsherlock.ActionBarSherlock;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.internal.ActionBarSherlockCompat;
import com.actionbarsherlock.internal.ActionBarSherlockNative;
import static com.xtremelabs.robolectric.Robolectric.shadowOf;
/**
* During initialization, {@link ActionBarSherlock} figures out which {@link
* com.actionbarsherlock.app.ActionBar} to use based on the API level. It does this by checking the
* Build.Version.SDK_INT value which depends on the hidden <i>SystemProperties</i> class.
*
* Because Roboelectric does not have this, it always returns <code>0</code> for its API level
* causing {@link ActionBarSherlock} to crash. This class helps resolve this issue by providing
* an {@link ActionBarSherlockNative} implementation for API level 0.
* @see ActionBarSherlock#registerImplementation(Class)
*/
@ActionBarSherlock.Implementation(api = 0)
public class ActionBarSherlockRobolectric extends ActionBarSherlockCompat {
final private ActionBar actionBar;
public ActionBarSherlockRobolectric(Activity activity, int flags) {
super(activity, flags);
actionBar = new MockActionBar(activity);
}
@Override public void setContentView(int layoutResId) {
LayoutInflater layoutInflater = LayoutInflater.from(mActivity);
View contentView = layoutInflater.inflate(layoutResId, null);
shadowOf(mActivity).setContentView(contentView);
}
@Override public void setContentView(View view) {
shadowOf(mActivity).setContentView(view);
}
@Override public ActionBar getActionBar() {
return actionBar;
}
@Override protected Context getThemedContext() {
return mActivity;
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test;
import com.squareup.test.actionbarsherlock.SherlockResourceLoader;
import com.xtremelabs.robolectric.RobolectricConfig;
import com.xtremelabs.robolectric.RobolectricTestRunner;
import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader;
import com.xtremelabs.robolectric.bytecode.ShadowWrangler;
import com.xtremelabs.robolectric.res.ResourceLoader;
import java.util.Arrays;
import org.junit.runners.model.InitializationError;
/**
* {@link RobolectricTestRunner} primarily used to provide a custom {@link
* RobolectricClassLoader} for instrumenting our own packages. It also provides a
* custom {@link ResourceLoader} to load ActionBarSherlock menus.
*/
public class ActionBarSherlockTestRunner extends RobolectricTestRunner {
private static RobolectricClassLoader classLoader;
protected ActionBarSherlockTestRunner(Class<?> testClass, RobolectricConfig robolectricConfig)
throws InitializationError {
super(testClass, ShadowWrangler.getInstance(), getClassLoader(), robolectricConfig);
}
/** Use this to add more packages that you want to be instrumented by Robolectric. */
private static RobolectricClassLoader getClassLoader() {
// Please don't cry.
if (classLoader == null && !isInstrumented()) {
classLoader = new RobolectricClassLoader(ShadowWrangler.getInstance(),
Arrays.asList("com.actionbarsherlock.app", "com.actionbarsherlock.view"));
}
return classLoader;
}
/** Return a {@link SherlockResourceLoader} instead, which loads ABS menus for testing. */
@Override protected ResourceLoader createResourceLoader(RobolectricConfig robolectricConfig) {
ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(robolectricConfig);
if (resourceLoader == null) {
try {
robolectricConfig.validate();
String rClassName = robolectricConfig.getRClassName();
Class rClass = Class.forName(rClassName);
resourceLoader = new SherlockResourceLoader(robolectricConfig.getRealSdkVersion(), rClass,
robolectricConfig.getResourcePath(), robolectricConfig.getAssetsDirectory());
resourceLoaderForRootAndDirectory.put(robolectricConfig, resourceLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
resourceLoader.setStrictI18n(robolectricConfig.getStrictI18n());
return resourceLoader;
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.SpinnerAdapter;
import com.actionbarsherlock.app.ActionBar;
public class MockActionBar extends ActionBar {
String title;
String subtitle;
View customView;
Context realContext;
public MockActionBar(Context context) {
realContext = context;
}
@Override public void setCustomView(View view) {
setCustomView(view, null);
}
@Override public void setCustomView(View view, LayoutParams layoutParams) {
this.customView = view;
}
@Override public void setCustomView(int resId) {
}
@Override public void setIcon(int resId) {
}
@Override public void setIcon(Drawable icon) {
}
@Override public void setLogo(int resId) {
}
@Override public void setLogo(Drawable logo) {
}
@Override
public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) {
}
@Override public void setSelectedNavigationItem(int position) {
}
@Override public int getSelectedNavigationIndex() {
return 0;
}
@Override public int getNavigationItemCount() {
return 0;
}
@Override public void setTitle(CharSequence title) {
this.title = (String) title;
}
@Override public void setTitle(int resId) {
title = realContext.getString(resId);
}
@Override public void setSubtitle(CharSequence newSubtitle) {
subtitle = (String) newSubtitle;
}
@Override public void setSubtitle(int resId) {
subtitle = realContext.getString(resId);
}
@Override public void setDisplayOptions(int options) {
}
@Override public void setDisplayOptions(int options, int mask) {
}
@Override public void setDisplayUseLogoEnabled(boolean useLogo) {
}
@Override public void setDisplayShowHomeEnabled(boolean showHome) {
}
@Override public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
}
@Override public void setDisplayShowTitleEnabled(boolean showTitle) {
}
@Override public void setDisplayShowCustomEnabled(boolean showCustom) {
}
@Override public void setBackgroundDrawable(Drawable d) {
}
@Override public View getCustomView() {
return customView;
}
@Override public CharSequence getTitle() {
return title;
}
@Override public CharSequence getSubtitle() {
return subtitle;
}
@Override public int getNavigationMode() {
return 0;
}
@Override public void setNavigationMode(int mode) {
}
@Override public int getDisplayOptions() {
return 0;
}
@Override public Tab newTab() {
return null;
}
@Override public void addTab(Tab tab) {
}
@Override public void addTab(Tab tab, boolean setSelected) {
}
@Override public void addTab(Tab tab, int position) {
}
@Override public void addTab(Tab tab, int position, boolean setSelected) {
}
@Override public void removeTab(Tab tab) {
}
@Override public void removeTabAt(int position) {
}
@Override public void removeAllTabs() {
}
@Override public void selectTab(Tab tab) {
}
@Override public Tab getSelectedTab() {
return null;
}
@Override public Tab getTabAt(int index) {
return null;
}
@Override public int getTabCount() {
return 0;
}
@Override public int getHeight() {
return 0;
}
@Override public void show() {
}
@Override public void hide() {
}
@Override public boolean isShowing() {
return false;
}
@Override public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
}
@Override
public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test.actionbarsherlock;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.MenuInflater;
import com.squareup.test.MockActionBar;
import com.xtremelabs.robolectric.internal.Implementation;
import com.xtremelabs.robolectric.internal.Implements;
import com.xtremelabs.robolectric.shadows.ShadowFragmentActivity;
@Implements(SherlockFragmentActivity.class)
public class ShadowSherlockFragmentActivity extends ShadowFragmentActivity {
private ActionBar actionBar;
@Implementation
public ActionBar getSupportActionBar() {
if (actionBar == null) {
actionBar = new MockActionBar(getApplicationContext());
}
return actionBar;
}
@Implementation
public MenuInflater getSupportMenuInflater() {
return new SherlockMenuInflater(getApplicationContext());
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test.actionbarsherlock;
import android.content.Context;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import static com.xtremelabs.robolectric.Robolectric.shadowOf;
/**
* Inflates menus that are part of ActionBarSherlock instead. Uses ABS custom {@link
* com.actionbarsherlock.app.view.Menu} instead of the stock one.
*/
public class SherlockMenuInflater extends MenuInflater {
private final Context context;
public SherlockMenuInflater(Context context) {
super(context);
this.context = context;
}
@Override public void inflate(int menuRes, Menu menu) {
SherlockResourceLoader sherlockResourceLoader =
(SherlockResourceLoader) shadowOf(context.getApplicationContext()).getResourceLoader();
sherlockResourceLoader.inflateSherlockMenu(context, menuRes, menu);
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test.actionbarsherlock;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.view.SubMenu;
import com.xtremelabs.robolectric.res.AttrResourceLoader;
import com.xtremelabs.robolectric.res.ResourceExtractor;
import com.xtremelabs.robolectric.res.XmlLoader;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import android.content.Context;
import android.text.TextUtils;
import com.xtremelabs.robolectric.tester.android.util.TestAttributeSet;
import com.xtremelabs.robolectric.util.I18nException;
/**
* Exact copy of the original {@link com.xtremelabs.robolectric.res.MenuLoader} provided by
* Robolectric, but instead uses the ABS {@link MenuItem} and the ABS {@link Menu}.
*/
public class SherlockMenuLoader extends XmlLoader {
private Map<String, MenuNode> menuNodesByMenuName = new HashMap<String, MenuNode>();
private AttrResourceLoader attrResourceLoader;
public SherlockMenuLoader(ResourceExtractor resourceExtractor,
AttrResourceLoader attrResourceLoader) {
super(resourceExtractor);
this.attrResourceLoader = attrResourceLoader;
}
@Override
protected void processResourceXml(File xmlFile, Document document, boolean ignored)
throws Exception {
MenuNode topLevelNode = new MenuNode("top-level", new HashMap<String, String>());
NodeList items = document.getChildNodes();
if (items.getLength() != 1) {
throw new RuntimeException(
"Expected only one top-level item in menu file " + xmlFile.getName());
}
if (items.item(0).getNodeName().compareTo("menu") != 0) {
throw new RuntimeException(
"Expected a top-level item called 'menu' in menu file " + xmlFile.getName());
}
processChildren(items.item(0).getChildNodes(), topLevelNode);
menuNodesByMenuName.put("menu/" + xmlFile.getName().replace(".xml", ""), topLevelNode);
}
private void processChildren(NodeList childNodes, MenuNode parent) {
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
processNode(node, parent);
}
}
private void processNode(Node node, MenuNode parent) {
String name = node.getNodeName();
NamedNodeMap attributes = node.getAttributes();
Map<String, String> attrMap = new HashMap<String, String>();
if (attributes != null) {
int length = attributes.getLength();
for (int i = 0; i < length; i++) {
Node attr = attributes.item(i);
attrMap.put(attr.getNodeName(), attr.getNodeValue());
}
}
if (!name.startsWith("#")) {
MenuNode menuNode = new MenuNode(name, attrMap);
parent.addChild(menuNode);
NodeList children = node.getChildNodes();
if (children != null && children.getLength() != 0) {
for (int i = 0; i < children.getLength(); i++) {
Node nodei = children.item(i);
if (childToIgnore(nodei)) {
continue;
} else if (validChildren(nodei)) {
// recursively add all nodes
processNode(nodei, menuNode);
} else {
throw new RuntimeException("Unknown menu node" + nodei.getNodeName());
}
}
}
}
}
private static boolean childToIgnore(Node nodei) {
return TextUtils.isEmpty(nodei.getNodeName()) || nodei.getNodeName().startsWith("#");
}
private static boolean validChildren(Node nodei) {
return nodei.getNodeName().equals("item")
|| nodei.getNodeName().equals("menu")
|| nodei.getNodeName().equals("group");
}
public boolean inflateMenu(Context context, String key, Menu root) {
return inflateMenu(context, key, null, root);
}
public boolean inflateMenu(Context context, int resourceId, Menu root) {
return inflateMenu(context, resourceExtractor.getResourceName(resourceId), root);
}
private boolean inflateMenu(Context context, String key, Map<String, String> attributes,
Menu root) {
MenuNode menuNode = menuNodesByMenuName.get(key);
if (menuNode == null) return false;
try {
if (attributes != null) {
for (Map.Entry<String, String> entry : attributes.entrySet()) {
if (!entry.getKey().equals("menu")) {
menuNode.attributes.put(entry.getKey(), entry.getValue());
}
}
}
menuNode.inflate(context, root);
return true;
} catch (I18nException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("error inflating " + key, e);
}
}
public class MenuNode {
private String name;
private final TestAttributeSet attributes;
private List<MenuNode> children = new ArrayList<MenuNode>();
public MenuNode(String name, Map<String, String> attributes) {
this.name = name;
this.attributes =
new TestAttributeSet(attributes, resourceExtractor, attrResourceLoader, null, false);
}
public List<MenuNode> getChildren() {
return children;
}
public void addChild(MenuNode MenuNode) {
children.add(MenuNode);
}
private boolean isSubMenuItem(MenuNode child) {
//List<MenuLoader.MenuNode> ch = child.children;
//return ch != null && ch.size() == 1 && "menu".equals(ch.get(0).name);
// TODO Must fix to support sub menu items.
return false;
}
private void addChildrenInGroup(MenuNode source, int groupId, Menu root) {
for (MenuNode child : source.children) {
String name = child.name;
TestAttributeSet attributes = child.attributes;
if (strictI18n) {
attributes.validateStrictI18n();
}
if (name.equals("item")) {
if (isSubMenuItem(child)) {
SubMenu sub =
root.addSubMenu(groupId, attributes.getAttributeResourceValue("android", "id", 0),
0, attributes.getAttributeValue("android", "title"));
MenuNode subMenuNode = child.children.get(0);
addChildrenInGroup(subMenuNode, groupId, sub);
} else {
MenuItem menuItem =
root.add(groupId, attributes.getAttributeResourceValue("android", "id", 0), 0,
attributes.getAttributeValue("android", "title"));
}
} else if (name.equals("group")) {
int newGroupId = attributes.getAttributeResourceValue("android", "id", 0);
addChildrenInGroup(child, newGroupId, root);
}
}
}
public void inflate(Context context, Menu root) throws Exception {
addChildrenInGroup(this, 0, root);
}
}
}
// Copyright 2012 Square, Inc.
package com.squareup.test.actionbarsherlock;
import android.content.Context;
import com.actionbarsherlock.view.Menu;
import com.xtremelabs.robolectric.res.AttrResourceLoader;
import com.xtremelabs.robolectric.res.DocumentLoader;
import com.xtremelabs.robolectric.res.ResourceExtractor;
import com.xtremelabs.robolectric.res.ResourceLoader;
import java.io.File;
import java.io.FileFilter;
import java.util.List;
/**
* Custom resource loader for ActionBarSherlock. It extends the original {@link ResourceLoader}.
* At this time we just load all menus into a {@link SherlockMenuLoader}.
*/
public class SherlockResourceLoader extends ResourceLoader {
private SherlockMenuLoader sherlockMenuLoader;
public SherlockResourceLoader(int sdkVersion, Class rClass, List<File> resourcePath,
File assetsDir) throws Exception {
super(sdkVersion, rClass, resourcePath, assetsDir);
ResourceExtractor resourceExtractor = new ResourceExtractor();
try {
resourceExtractor.addLocalRClass(rClass);
resourceExtractor.addSystemRClass(android.R.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
sherlockMenuLoader =
new SherlockMenuLoader(resourceExtractor, new AttrResourceLoader(resourceExtractor));
}
@Override protected void loadOtherResources(File resourceDir) {
DocumentLoader loader = new DocumentLoader(sherlockMenuLoader);
try {
loader.loadResourceXmlDirs(resourceDir.listFiles(new FileFilter() {
@Override public boolean accept(File file) {
return file.getPath().contains(File.separator + "menu");
}
}));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void inflateSherlockMenu(Context context, int resource, Menu root) {
sherlockMenuLoader.inflateMenu(context, resource, root);
}
}
package com.squareup.test.actionbarsherlock;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.ContextMenu;
import android.view.View;
import com.actionbarsherlock.view.ActionProvider;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.view.SubMenu;
import com.xtremelabs.robolectric.Robolectric;
/**
* Copied from {@link com.xtremelabs.robolectric.tester.android.view.TestMenuItem}.
*/
public class TestSherlockMenuItem implements MenuItem {
private int itemId;
private CharSequence title;
private boolean enabled = true;
private OnMenuItemClickListener menuItemClickListener;
public int iconRes;
private Intent intent;
private SubMenu subMenu;
public TestSherlockMenuItem() {
super();
}
public TestSherlockMenuItem(int itemId) {
super();
this.itemId = itemId;
}
public void setItemId(int itemId) {
this.itemId = itemId;
}
@Override
public int getItemId() {
return itemId;
}
@Override
public int getGroupId() {
return 0;
}
@Override
public int getOrder() {
return 0;
}
@Override
public MenuItem setTitle(CharSequence title) {
this.title = title;
return this;
}
@Override
public MenuItem setTitle(int title) {
return null;
}
@Override
public CharSequence getTitle() {
return title;
}
@Override
public MenuItem setTitleCondensed(CharSequence title) {
return null;
}
@Override
public CharSequence getTitleCondensed() {
return null;
}
@Override
public MenuItem setIcon(Drawable icon) {
return null;
}
@Override
public MenuItem setIcon(int iconRes) {
this.iconRes = iconRes;
return this;
}
@Override
public Drawable getIcon() {
return null;
}
@Override
public MenuItem setIntent(Intent intent) {
this.intent = intent;
return this;
}
@Override
public Intent getIntent() {
return this.intent;
}
@Override
public MenuItem setShortcut(char numericChar, char alphaChar) {
return null;
}
@Override
public MenuItem setNumericShortcut(char numericChar) {
return null;
}
@Override
public char getNumericShortcut() {
return 0;
}
@Override
public MenuItem setAlphabeticShortcut(char alphaChar) {
return null;
}
@Override
public char getAlphabeticShortcut() {
return 0;
}
@Override
public MenuItem setCheckable(boolean checkable) {
return null;
}
@Override
public boolean isCheckable() {
return false;
}
@Override
public MenuItem setChecked(boolean checked) {
return null;
}
@Override
public boolean isChecked() {
return false;
}
@Override
public MenuItem setVisible(boolean visible) {
return null;
}
@Override
public boolean isVisible() {
return false;
}
@Override
public MenuItem setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public boolean hasSubMenu() {
return subMenu != null;
}
@Override
public SubMenu getSubMenu() {
return subMenu;
}
public void setSubMenu(SubMenu subMenu) {
this.subMenu = subMenu;
}
@Override
public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) {
this.menuItemClickListener = menuItemClickListener;
return this;
}
@Override
public ContextMenu.ContextMenuInfo getMenuInfo() {
return null;
}
@Override
public void setShowAsAction(int i) {
}
@Override
public MenuItem setShowAsActionFlags(int i) {
return null;
}
@Override
public MenuItem setActionView(View view) {
return null;
}
@Override
public MenuItem setActionView(int i) {
return null;
}
@Override
public View getActionView() {
return null;
}
@Override
public MenuItem setActionProvider(ActionProvider actionProvider) {
return null;
}
@Override
public ActionProvider getActionProvider() {
return null;
}
@Override
public boolean expandActionView() {
return false;
}
@Override
public boolean collapseActionView() {
return false;
}
@Override
public boolean isActionViewExpanded() {
return false;
}
@Override
public MenuItem setOnActionExpandListener(OnActionExpandListener onActionExpandListener) {
return null;
}
public void click() {
if (enabled && menuItemClickListener != null) {
menuItemClickListener.onMenuItemClick(this);
} else if (enabled && intent != null) {
Robolectric.application.startActivity(intent);
}
}
}
@javierLiarte
Copy link

Thanks Jake, @leviboostian and xian ;)

Although your gist is no longer accesible I think I've finally got this working. I'm using xian's fork gist with robolectric v2.1 (tested and working also with 2.1.1; in 2.2 I'm getting NPE) and ABS 4.3.1.

I've had to change Implementation import in SherlockMenuInflater that is what I suppose you did:

//import org.robolectric.internal.Implementation;
import org.robolectric.annotation.Implementation;

@tokunbo
Copy link

tokunbo commented Oct 9, 2013

Can anyone get this to work with ABS4.4.0 & robolectric-2.1.1.jar? What do you guys have for @RunWith()? I've tried all I can think of, but I just keep getting errors.

Caused by: java.lang.RuntimeException: Couldn't find content container view
at com.actionbarsherlock.internal.ActionBarSherlockCompat.generateLayout(ActionBarSherlockCompat.java:1015)
at com.actionbarsherlock.internal.ActionBarSherlockCompat.installDecor(ActionBarSherlockCompat.java:902)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment