首页 > 代码库 > 使用Robolectric对Android应用进行单元测试

使用Robolectric对Android应用进行单元测试

Google提供了Android Testing framework,但是需要模拟器或者真机去跑,速度较慢。要做纯净的unit test,项目代码里面又有很多Android API的依赖,太难测。上网搜了一下,要将java的code和Android的code区分开,好像只有Robolectric能做到。

两篇参考文章

http://stackoverflow.com/questions/14949480/android-unit-test-case-automation-robolectric-library-vs-android-testing-framew

http://simpleprogrammer.com/2010/07/27/the-best-way-to-unit-test-in-android/

 

Robolectric搭建:(官网参考)

1. 创建一个test project。

2. 将android.jar (API 17)和robolectric-2.4-jar-with-dependencies.jar添加到libs。(运行测试时会自动下载测试依赖的jar包,文件保存在maven仓库。)

3. 选择test project的properties > Java Build Path > Projects > Add要测试的project。

4. 选择Libraries,添加上面两个jar和JUnit 4。

5. 运行测试用例的时候选择Run Configurations > Arguments > Working directory > Other > Workspace,选择待测project。这是为了解决找不到AndroidManifest.xml的错误提示。

 

做了demo,跑通了。

技术分享
package com.mstr.robolectric;import static org.hamcrest.CoreMatchers.equalTo;import static org.junit.Assert.assertThat;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.robolectric.Robolectric;import org.robolectric.RobolectricTestRunner;import org.robolectric.annotation.Config;import android.widget.Button;import com.example.trasparentdialogactivity.R;import com.example.trasparentdialogactivity.TransparentDialogActivity;@Config(emulateSdk = 17)@RunWith(RobolectricTestRunner.class)public class TestDemo {    private TransparentDialogActivity activity;    @Before    public void setup()    {        this.activity = Robolectric.buildActivity(TransparentDialogActivity.class).create().get();    }    @Test    public void shouldHaveHappySmiles() throws Exception     {        Button startSVButton =(Button) activity.findViewById(R.id.startSVButton);        String startSVButtonText = startSVButton.getText().toString();        assertThat(startSVButtonText, equalTo("startSV"));                String hello = this.activity.getPackageName();        assertThat(hello, equalTo("Hello world!"));    }    }
View Code

 

为产品项目建了个类似的test project,却总提示缺少Missing required <application/> element in xxxxxx AndroidManifest.xml。这些xml文件都是依赖项目中的,每次运行测试Robolectric都会去check哪些library projects。Google了一把,找到了一个workround。(链接)

MapzenAndroidManifest.java文件直接粘过来,改下package就好。

技术分享
package com.mstr.robolectric;import android.app.Activity;import android.graphics.Color;import org.robolectric.AndroidManifest;import org.robolectric.res.*;import org.w3c.dom.Document;import org.w3c.dom.NamedNodeMap;import org.w3c.dom.Node;import org.w3c.dom.NodeList;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.util.ArrayList;import java.util.Collection;import java.util.Collections;import java.util.HashMap;import java.util.LinkedHashMap;import java.util.LinkedHashSet;import java.util.List;import java.util.Map;import java.util.Properties;import static android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP;import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;import static android.content.pm.ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING;import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE;import static android.content.pm.ApplicationInfo.FLAG_HAS_CODE;import static android.content.pm.ApplicationInfo.FLAG_KILL_AFTER_RESTORE;import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;import static android.content.pm.ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS;import static android.content.pm.ApplicationInfo.FLAG_RESTORE_ANY_VERSION;import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS;import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS;import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES;import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS;import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;import static android.content.pm.ApplicationInfo.FLAG_VM_SAFE_MODE;/** * Original source copied from https://github.com/robolectric/robolectric/blob/master/src/main/java/org/robolectric/AndroidManifest.java. * <p /> * Modifies {@link #parseAndroidManifest()} to maintain backward compatibility with library projects * that do not yet include the <code>&lt;application/&gt;</code> element in AndroidManifest.xml. */public class MapzenAndroidManifest extends AndroidManifest {  public static final String DEFAULT_MANIFEST_NAME = "AndroidManifest.xml";  public static final String DEFAULT_RES_FOLDER = "res";  public static final String DEFAULT_ASSETS_FOLDER = "assets";  private final FsFile androidManifestFile;  private final FsFile resDirectory;  private final FsFile assetsDirectory;  private boolean manifestIsParsed;  private String applicationName;  private String applicationLabel;  private String rClassName;  private String packageName;  private String processName;  private String themeRef;  private String labelRef;  private Integer targetSdkVersion;  private Integer minSdkVersion;  private int versionCode;  private String versionName;  private int applicationFlags;  private final List<ContentProviderData> providers = new ArrayList<ContentProviderData>();  private final List<ReceiverAndIntentFilter> receivers = new ArrayList<ReceiverAndIntentFilter>();  private final Map<String, ActivityData> activityDatas = new LinkedHashMap<String, ActivityData>();  private final List<String> usedPermissions = new ArrayList<String>();  private MetaData applicationMetaData;  private List<FsFile> libraryDirectories;  private List<AndroidManifest> libraryManifests;  /**   * Creates a Robolectric configuration using default Android files relative to the specified base directory.   * <p/>   * The manifest will be baseDir/AndroidManifest.xml, res will be baseDir/res, and assets in baseDir/assets.   *   * @param baseDir the base directory of your Android project   * @deprecated Use {@link #MapzenAndroidManifest(org.robolectric.res.FsFile, org.robolectric.res.FsFile, org.robolectric.res.FsFile)} instead.}   */  public MapzenAndroidManifest(final File baseDir) {    this(Fs.newFile(baseDir));  }  public MapzenAndroidManifest(final FsFile androidManifestFile, final FsFile resDirectory) {    this(androidManifestFile, resDirectory, resDirectory.getParent().join(DEFAULT_ASSETS_FOLDER));  }  /**   * @deprecated Use {@link #MapzenAndroidManifest(org.robolectric.res.FsFile, org.robolectric.res.FsFile, org.robolectric.res.FsFile)} instead.}   */  public MapzenAndroidManifest(final FsFile baseDir) {    this(baseDir.join(DEFAULT_MANIFEST_NAME), baseDir.join(DEFAULT_RES_FOLDER),        baseDir.join(DEFAULT_ASSETS_FOLDER));  }  /**   * Creates a Robolectric configuration using specified locations.   *   * @param androidManifestFile location of the AndroidManifest.xml file   * @param resDirectory        location of the res directory   * @param assetsDirectory     location of the assets directory   */  public MapzenAndroidManifest(FsFile androidManifestFile, FsFile resDirectory,          FsFile assetsDirectory) {    super(androidManifestFile, resDirectory, assetsDirectory);    this.androidManifestFile = androidManifestFile;    this.resDirectory = resDirectory;    this.assetsDirectory = assetsDirectory;  }  public String getThemeRef(Class<? extends Activity> activityClass) {    ActivityData activityData = getActivityData(activityClass.getName());    String themeRef = activityData != null ? activityData.getThemeRef() : null;    if (themeRef == null) {      themeRef = getThemeRef();    }    return themeRef;  }  public String getRClassName() throws Exception {    parseAndroidManifest();    return rClassName;  }  public Class getRClass() {    try {      String rClassName = getRClassName();      return Class.forName(rClassName);    } catch (Exception e) {      return null;    }  }  public void validate() {    if (!androidManifestFile.exists() || !androidManifestFile.isFile()) {      throw new RuntimeException(androidManifestFile + " not found or not a file; it should point to your project‘s AndroidManifest.xml");    }  }  @Override  public void parseAndroidManifest() {    if (manifestIsParsed) {      return;    }    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();    Document manifestDocument = null;    try {      DocumentBuilder db = dbf.newDocumentBuilder();      InputStream inputStream = androidManifestFile.getInputStream();      manifestDocument = db.parse(inputStream);      inputStream.close();    } catch (Exception ignored) {      ignored.printStackTrace();    }    // Disables strict enforcement of "application" tag requirement for library projects.    // if(manifestDocument.getElementsByTagName("application").item(0) == null) {    //   throw new IllegalArgumentException("Missing required <application/> element in " + androidManifestFile.getPath());    // }    if (packageName == null) {      packageName = getTagAttributeText(manifestDocument, "manifest", "package");    }    versionCode = getTagAttributeIntValue(manifestDocument, "manifest", "android:versionCode", 0);    versionName = getTagAttributeText(manifestDocument, "manifest", "android:versionName");    rClassName = packageName + ".R";    applicationName = getTagAttributeText(manifestDocument, "application", "android:name");    applicationLabel = getTagAttributeText(manifestDocument, "application", "android:label");    minSdkVersion = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:minSdkVersion");    targetSdkVersion = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:targetSdkVersion");    processName = getTagAttributeText(manifestDocument, "application", "android:process");    if (processName == null) {      processName = packageName;    }    themeRef = getTagAttributeText(manifestDocument, "application", "android:theme");    labelRef = getTagAttributeText(manifestDocument, "application", "android:label");    parseApplicationFlags(manifestDocument);    parseReceivers(manifestDocument);    parseActivities(manifestDocument);    parseApplicationMetaData(manifestDocument);    parseContentProviders(manifestDocument);    parseUsedPermissions(manifestDocument);    manifestIsParsed = true;  }  private void parseUsedPermissions(Document manifestDocument) {    NodeList elementsByTagName = manifestDocument.getElementsByTagName("uses-permission");    int length = elementsByTagName.getLength();    for (int i = 0; i < length; i++) {      Node node = elementsByTagName.item(i).getAttributes().getNamedItem("android:name");      usedPermissions.add(node.getNodeValue());    }  }  private void parseContentProviders(Document manifestDocument) {    Node application = manifestDocument.getElementsByTagName("application").item(0);    if (application == null) return;    for (Node contentProviderNode : getChildrenTags(application, "provider")) {      Node nameItem = contentProviderNode.getAttributes().getNamedItem("android:name");      Node authorityItem = contentProviderNode.getAttributes().getNamedItem("android:authorities");      if (nameItem != null && authorityItem != null) {        providers.add(new ContentProviderData(resolveClassRef(nameItem.getTextContent()), authorityItem.getTextContent()));      }    }  }  private void parseReceivers(final Document manifestDocument) {    Node application = manifestDocument.getElementsByTagName("application").item(0);    if (application == null) return;    for (Node receiverNode : getChildrenTags(application, "receiver")) {      Node namedItem = receiverNode.getAttributes().getNamedItem("android:name");      if (namedItem == null) continue;      String receiverName = resolveClassRef(namedItem.getTextContent());      MetaData metaData = new MetaData(getChildrenTags(receiverNode, "meta-data"));      for (Node intentFilterNode : getChildrenTags(receiverNode, "intent-filter")) {        List<String> actions = new ArrayList<String>();        for (Node actionNode : getChildrenTags(intentFilterNode, "action")) {          Node nameNode = actionNode.getAttributes().getNamedItem("android:name");          if (nameNode != null) {            actions.add(nameNode.getTextContent());          }        }        receivers.add(new ReceiverAndIntentFilter(receiverName, actions, metaData));      }    }  }  private void parseActivities(final Document manifestDocument) {    Node application = manifestDocument.getElementsByTagName("application").item(0);    if (application == null) return;    for (Node activityNode : getChildrenTags(application, "activity")) {      parseActivity(activityNode, false);    }    for (Node activityNode : getChildrenTags(application, "activity-alias")) {      parseActivity(activityNode, true);    }  }  private void parseActivity(Node activityNode, boolean isAlias) {    final NamedNodeMap attributes = activityNode.getAttributes();    final int attrCount = attributes.getLength();    final List<IntentFilterData> intentFilterData =http://www.mamicode.com/ parseIntentFilters(activityNode);    final HashMap<String, String> activityAttrs = new HashMap<String, String>(attrCount);    for(int i = 0; i < attrCount; i++) {      Node attr = attributes.item(i);      String v = attr.getNodeValue();      if( v != null) {        activityAttrs.put(attr.getNodeName(), v);      }    }    String activityName = resolveClassRef(activityAttrs.get(ActivityData.getNameAttr("android")));    if (activityName == null) {      return;    }    ActivityData targetActivity = null;    if (isAlias) {      String targetName = resolveClassRef(activityAttrs.get(ActivityData.getTargetAttr("android")));      if (activityName == null) {        return;      }      // The target activity should have been parsed already so if it exists we should find it in      // activityDatas.      targetActivity = activityDatas.get(targetName);      activityAttrs.put(ActivityData.getTargetAttr("android"), targetName);    }    activityAttrs.put(ActivityData.getNameAttr("android"), activityName);    activityDatas.put(activityName, new ActivityData("android", activityAttrs, intentFilterData, targetActivity));  }  private List<IntentFilterData> parseIntentFilters(final Node activityNode) {    ArrayList<IntentFilterData> intentFilterDatas = new ArrayList<IntentFilterData>();    for (Node n : getChildrenTags(activityNode, "intent-filter")) {      ArrayList<String> actionNames = new ArrayList<String>();      ArrayList<String> categories = new ArrayList<String>();      //should only be one action.      for (Node action : getChildrenTags(n, "action")) {        NamedNodeMap attributes = action.getAttributes();        Node actionNameNode = attributes.getNamedItem("android:name");        if (actionNameNode != null) {          actionNames.add(actionNameNode.getNodeValue());        }      }      for (Node category : getChildrenTags(n, "category")) {        NamedNodeMap attributes = category.getAttributes();        Node categoryNameNode = attributes.getNamedItem("android:name");        if (categoryNameNode != null) {          categories.add(categoryNameNode.getNodeValue());        }      }      IntentFilterData intentFilterData = new IntentFilterData(actionNames, categories);      intentFilterData = parseIntentFilterData(n, intentFilterData);      intentFilterDatas.add(intentFilterData);    }    return intentFilterDatas;  }  private IntentFilterData parseIntentFilterData(final Node intentFilterNode, IntentFilterData intentFilterData) {    for (Node n : getChildrenTags(intentFilterNode, "data")) {      NamedNodeMap attributes = n.getAttributes();      String host = null;      String port = null;      Node schemeNode = attributes.getNamedItem("android:scheme");      if (schemeNode != null) {        intentFilterData.addScheme(schemeNode.getNodeValue());      }      Node hostNode = attributes.getNamedItem("android:host");      if (hostNode != null) {        host = hostNode.getNodeValue();      }      Node portNode = attributes.getNamedItem("android:port");      if (portNode != null) {        port = portNode.getNodeValue();      }      intentFilterData.addAuthority(host, port);      Node pathNode = attributes.getNamedItem("android:path");      if (pathNode != null) {        intentFilterData.addPath(pathNode.getNodeValue());      }      Node pathPatternNode = attributes.getNamedItem("android:pathPattern");      if (pathPatternNode != null) {        intentFilterData.addPathPattern(pathPatternNode.getNodeValue());      }      Node pathPrefixNode = attributes.getNamedItem("android:pathPrefix");      if (pathPrefixNode != null) {        intentFilterData.addPathPrefix(pathPrefixNode.getNodeValue());      }      Node mimeTypeNode = attributes.getNamedItem("android:mimeType");      if (mimeTypeNode != null) {        intentFilterData.addMimeType(mimeTypeNode.getNodeValue());      }    }    return intentFilterData;  }  /***   * Attempt to parse a string in to it‘s appropriate type   * @param value Value to parse   * @return Parsed result   */  private static Object parseValue(String value) {    if (value =http://www.mamicode.com/= null) {      return null;    } else if ("true".equals(value)) {      return true;    } else if ("false".equals(value)) {      return false;    } else if (value.startsWith("#")) {      // if it‘s a color, add it and continue      try {        return Color.parseColor(value);      } catch (IllegalArgumentException e) {            /* Not a color */      }    } else if (value.contains(".")) {      // most likely a float      try {        return Float.parseFloat(value);      } catch (NumberFormatException e) {        // Not a float      }    } else {      // if it‘s an int, add it and continue      try {        return Integer.parseInt(value);      } catch (NumberFormatException ei) {        // Not an int      }    }    // Not one of the above types, keep as String    return value;  }  /***   * Allows {@link org.robolectric.res.builder.RobolectricPackageManager} to provide   * a resource index for initialising the resource attributes in all the metadata elements   * @param resLoader used for getting resource IDs from string identifiers   */  public void initMetaData(ResourceLoader resLoader) {    applicationMetaData.init(resLoader, packageName);    for (ReceiverAndIntentFilter receiver : receivers) {      receiver.metaData.init(resLoader, packageName);    }  }  private void parseApplicationMetaData(final Document manifestDocument) {    Node application = manifestDocument.getElementsByTagName("application").item(0);    if (application == null) return;    applicationMetaData = new MetaData(getChildrenTags(application, "meta-data"));  }  private String resolveClassRef(String maybePartialClassName) {    return (maybePartialClassName.startsWith(".")) ? packageName + maybePartialClassName : maybePartialClassName;  }  private List<Node> getChildrenTags(final Node node, final String tagName) {    List<Node> children = new ArrayList<Node>();    for (int i = 0; i < node.getChildNodes().getLength(); i++) {      Node childNode = node.getChildNodes().item(i);      if (childNode.getNodeName().equalsIgnoreCase(tagName)) {        children.add(childNode);      }    }    return children;  }  private void parseApplicationFlags(final Document manifestDocument) {    applicationFlags = getApplicationFlag(manifestDocument, "android:allowBackup", FLAG_ALLOW_BACKUP);    applicationFlags += getApplicationFlag(manifestDocument, "android:allowClearUserData", FLAG_ALLOW_CLEAR_USER_DATA);    applicationFlags += getApplicationFlag(manifestDocument, "android:allowTaskReparenting", FLAG_ALLOW_TASK_REPARENTING);    applicationFlags += getApplicationFlag(manifestDocument, "android:debuggable", FLAG_DEBUGGABLE);    applicationFlags += getApplicationFlag(manifestDocument, "android:hasCode", FLAG_HAS_CODE);    applicationFlags += getApplicationFlag(manifestDocument, "android:killAfterRestore", FLAG_KILL_AFTER_RESTORE);    applicationFlags += getApplicationFlag(manifestDocument, "android:persistent", FLAG_PERSISTENT);    applicationFlags += getApplicationFlag(manifestDocument, "android:resizeable", FLAG_RESIZEABLE_FOR_SCREENS);    applicationFlags += getApplicationFlag(manifestDocument, "android:restoreAnyVersion", FLAG_RESTORE_ANY_VERSION);    applicationFlags += getApplicationFlag(manifestDocument, "android:largeScreens", FLAG_SUPPORTS_LARGE_SCREENS);    applicationFlags += getApplicationFlag(manifestDocument, "android:normalScreens", FLAG_SUPPORTS_NORMAL_SCREENS);    applicationFlags += getApplicationFlag(manifestDocument, "android:anyDensity", FLAG_SUPPORTS_SCREEN_DENSITIES);    applicationFlags += getApplicationFlag(manifestDocument, "android:smallScreens", FLAG_SUPPORTS_SMALL_SCREENS);    applicationFlags += getApplicationFlag(manifestDocument, "android:testOnly", FLAG_TEST_ONLY);    applicationFlags += getApplicationFlag(manifestDocument, "android:vmSafeMode", FLAG_VM_SAFE_MODE);  }  private int getApplicationFlag(final Document doc, final String attribute, final int attributeValue) {    String flagString = getTagAttributeText(doc, "application", attribute);    return "true".equalsIgnoreCase(flagString) ? attributeValue : 0;  }  private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute) {    return getTagAttributeIntValue(doc, tag, attribute, null);  }  private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute, final Integer defaultValue) {    String valueString = getTagAttributeText(doc, tag, attribute);    if (valueString != null) {      return Integer.parseInt(valueString);    }    return defaultValue;  }  public String getApplicationName() {    parseAndroidManifest();    return applicationName;  }  public String getActivityLabel(Class<? extends Activity> activity) {    parseAndroidManifest();    ActivityData data = getActivityData(activity.getName());    return (data != null && data.getLabel() != null) ? data.getLabel() : applicationLabel;  }  public void setPackageName(String packageName) {    this.packageName = packageName;  }  public String getPackageName() {    parseAndroidManifest();    return packageName;  }  public int getVersionCode() {    return versionCode;  }  public String getVersionName() {    return versionName;  }  public String getLabelRef() {    return labelRef;  }  public int getMinSdkVersion() {    parseAndroidManifest();    return minSdkVersion == null ? 1 : minSdkVersion;  }  public int getTargetSdkVersion() {    parseAndroidManifest();    return targetSdkVersion == null ? getMinSdkVersion() : targetSdkVersion;  }  public int getApplicationFlags() {    parseAndroidManifest();    return applicationFlags;  }  public String getProcessName() {    parseAndroidManifest();    return processName;  }  public Map<String, Object> getApplicationMetaData() {    parseAndroidManifest();    return applicationMetaData.valueMap;  }  public ResourcePath getResourcePath() {    validate();    return new ResourcePath(getRClass(), getPackageName(), resDirectory, assetsDirectory);  }  public List<ResourcePath> getIncludedResourcePaths() {    Collection<ResourcePath> resourcePaths = new LinkedHashSet<ResourcePath>(); // Needs stable ordering and no duplicates    resourcePaths.add(getResourcePath());    for (AndroidManifest libraryManifest : getLibraryManifests()) {      resourcePaths.addAll(libraryManifest.getIncludedResourcePaths());    }    return new ArrayList<ResourcePath>(resourcePaths);  }  public List<ContentProviderData> getContentProviders() {    parseAndroidManifest();    return providers;  }  public void setLibraryDirectories(List<FsFile> libraryDirectories) {    this.libraryDirectories = libraryDirectories;  }  protected void createLibraryManifests() {    libraryManifests = new ArrayList<AndroidManifest>();    if (libraryDirectories == null) {      libraryDirectories = findLibraries();    }    for (FsFile libraryBaseDir : libraryDirectories) {      MapzenAndroidManifest libraryManifest = createLibraryAndroidManifest(libraryBaseDir);      libraryManifest.createLibraryManifests();      libraryManifests.add(libraryManifest);    }  }  protected List<FsFile> findLibraries() {    FsFile baseDir = getBaseDir();    List<FsFile> libraryBaseDirs = new ArrayList<FsFile>();    Properties properties = getProperties(baseDir.join("project.properties"));    // get the project.properties overrides and apply them (if any)    Properties overrideProperties = getProperties(baseDir.join("test-project.properties"));    if (overrideProperties!=null) properties.putAll(overrideProperties);    if (properties != null) {      int libRef = 1;      String lib;      while ((lib = properties.getProperty("android.library.reference." + libRef)) != null) {        FsFile libraryBaseDir = baseDir.join(lib);        if (libraryBaseDir.isDirectory()) {          // Ignore directories without any files          FsFile[] libraryBaseDirFiles = libraryBaseDir.listFiles();          if (libraryBaseDirFiles != null && libraryBaseDirFiles.length > 0) {            libraryBaseDirs.add(libraryBaseDir);          }        }        libRef++;      }    }    return libraryBaseDirs;  }  protected FsFile getBaseDir() {    return getResDirectory().getParent();  }  protected MapzenAndroidManifest createLibraryAndroidManifest(FsFile libraryBaseDir) {    return new MapzenAndroidManifest(libraryBaseDir);  }  public List<AndroidManifest> getLibraryManifests() {    if (libraryManifests == null) createLibraryManifests();    return Collections.unmodifiableList(libraryManifests);  }  private static Properties getProperties(FsFile propertiesFile) {    if (!propertiesFile.exists()) return null;    Properties properties = new Properties();    InputStream stream;    try {      stream = propertiesFile.getInputStream();    } catch (IOException e) {      throw new RuntimeException(e);    }    try {      try {        properties.load(stream);      } finally {        stream.close();      }    } catch (IOException e) {      throw new RuntimeException(e);    }    return properties;  }  public FsFile getResDirectory() {    return resDirectory;  }  public FsFile getAssetsDirectory() {    return assetsDirectory;  }  public int getReceiverCount() {    parseAndroidManifest();    return receivers.size();  }  public String getReceiverClassName(final int receiverIndex) {    parseAndroidManifest();    return receivers.get(receiverIndex).getBroadcastReceiverClassName();  }  public List<String> getReceiverIntentFilterActions(final int receiverIndex) {    parseAndroidManifest();    return receivers.get(receiverIndex).getIntentFilterActions();  }  public Map<String, Object> getReceiverMetaData(final int receiverIndex) {    parseAndroidManifest();    return receivers.get(receiverIndex).getMetaData().valueMap;  }  private static String getTagAttributeText(final Document doc, final String tag, final String attribute) {    NodeList elementsByTagName = doc.getElementsByTagName(tag);    for (int i = 0; i < elementsByTagName.getLength(); ++i) {      Node item = elementsByTagName.item(i);      Node namedItem = item.getAttributes().getNamedItem(attribute);      if (namedItem != null) {        return namedItem.getTextContent();      }    }    return null;  }  @Override  public boolean equals(Object o) {    if (this == o) return true;    if (o == null || getClass() != o.getClass()) return false;    MapzenAndroidManifest that = (MapzenAndroidManifest) o;    if (androidManifestFile != null ? !androidManifestFile.equals(that.androidManifestFile) : that.androidManifestFile != null)      return false;    if (assetsDirectory != null ? !assetsDirectory.equals(that.assetsDirectory) : that.assetsDirectory != null)      return false;    if (resDirectory != null ? !resDirectory.equals(that.resDirectory) : that.resDirectory != null) return false;    return true;  }  @Override  public int hashCode() {    int result = androidManifestFile != null ? androidManifestFile.hashCode() : 0;    result = 31 * result + (resDirectory != null ? resDirectory.hashCode() : 0);    result = 31 * result + (assetsDirectory != null ? assetsDirectory.hashCode() : 0);    return result;  }  public ActivityData getActivityData(String activityClassName) {    return activityDatas.get(activityClassName);  }  public String getThemeRef() {    return themeRef;  }  public Map<String, ActivityData> getActivityDatas() {    parseAndroidManifest();    return activityDatas;  }  public List<String> getUsedPermissions() {    parseAndroidManifest();    return usedPermissions;  }  private static final class MetaData {    private final Map<String, Object> valueMap = new LinkedHashMap<String, Object>();    private final Map<String, VALUE_TYPE> typeMap = new LinkedHashMap<String, VALUE_TYPE>();    private boolean initialised;    public MetaData(List<Node> nodes) {      for (Node metaNode : nodes) {        NamedNodeMap attributes = metaNode.getAttributes();        Node nameAttr = attributes.getNamedItem("android:name");        Node valueAttr = attributes.getNamedItem("android:value");        Node resourceAttr = attributes.getNamedItem("android:resource");        if (valueAttr != null) {          valueMap.put(nameAttr.getNodeValue(), valueAttr.getNodeValue());          typeMap.put(nameAttr.getNodeValue(), VALUE_TYPE.VALUE);        } else if (resourceAttr != null) {          valueMap.put(nameAttr.getNodeValue(), resourceAttr.getNodeValue());          typeMap.put(nameAttr.getNodeValue(), VALUE_TYPE.RESOURCE);        }      }    }    public void init(ResourceLoader resLoader, String packageName) {      ResourceIndex resIndex = resLoader.getResourceIndex();      if (!initialised) {        for (Map.Entry<String,MetaData.VALUE_TYPE> entry : typeMap.entrySet()) {          String value = valueMap.get(entry.getKey()).toString();          if (value.startsWith("@")) {            ResName resName = ResName.qualifyResName(value.substring(1), packageName, null);            switch (entry.getValue()) {              case RESOURCE:                // Was provided by resource attribute, store resource ID                valueMap.put(entry.getKey(), resIndex.getResourceId(resName));                break;              case VALUE:                // Was provided by value attribute, need to parse it                TypedResource<?> typedRes = resLoader.getValue(resName, "");                // The typed resource‘s data is always a String, so need to parse the value.                switch (typedRes.getResType()) {                  case BOOLEAN: case COLOR: case INTEGER: case FLOAT:                    valueMap.put(entry.getKey(),parseValue(typedRes.getData().toString()));                    break;                  default:                    valueMap.put(entry.getKey(),typedRes.getData());                }                break;            }          } else if (entry.getValue() == MetaData.VALUE_TYPE.VALUE) {            // Raw value, so parse it in to the appropriate type and store it            valueMap.put(entry.getKey(), parseValue(value));          }        }        // Finished parsing, mark as initialised        initialised = true;      }    }    private enum VALUE_TYPE {      RESOURCE,      VALUE    }  }  private static class ReceiverAndIntentFilter {    private final List<String> intentFilterActions;    private final String broadcastReceiverClassName;    private final MetaData metaData;    public ReceiverAndIntentFilter(final String broadcastReceiverClassName, final List<String> intentFilterActions, final MetaData metaData) {      this.broadcastReceiverClassName = broadcastReceiverClassName;      this.intentFilterActions = intentFilterActions;      this.metaData =http://www.mamicode.com/ metaData;    }    public String getBroadcastReceiverClassName() {      return broadcastReceiverClassName;    }    public List<String> getIntentFilterActions() {      return intentFilterActions;    }    public MetaData getMetaData() {      return metaData;    }  }}
View Code

MapzenTestRunner.java就保留override的createAppManifest方法就行了,其他报错的注掉就好。

技术分享
package com.mstr.robolectric;//import com.mapzen.open.shadows.ShadowGLMatrix;//import com.mapzen.open.shadows.ShadowGLShader;//import com.mapzen.open.shadows.ShadowGLState;//import com.mapzen.open.shadows.ShadowMapView;//import com.mapzen.open.shadows.ShadowMint;//import com.mapzen.open.shadows.ShadowVectorTileLayer;import org.junit.runners.model.InitializationError;import org.robolectric.AndroidManifest;import org.robolectric.RobolectricTestRunner;import org.robolectric.bytecode.ClassInfo;import org.robolectric.bytecode.Setup;import org.robolectric.bytecode.ShadowMap;import org.robolectric.res.FsFile;import com.mstr.robolectric.MapzenAndroidManifest;import java.util.Arrays;import java.util.Collections;import java.util.List;/** * Custom unit test runner. Enables custom shadows for testing third party library integrations. * <p/> * <strong>Adding a Custom Shadow</strong> * <ol> * <li>Create a new custom shadow class using instructions outlined here: * http://robolectric.blogspot.com/2011/01/how-to-create-your-own-shadow-classes.html</li> * <li>Map the shadow class to the original using * {@link org.robolectric.annotation.Implements}.</li> * <li>Customize behavior of the shadow using * {@link org.robolectric.annotation.Implementation}.</li> * <li>Add the original class name to the {@link #CUSTOM_SHADOW_TARGETS} list.</li> * <li>Bind the shadow class by calling * {@link ShadowMap.Builder#addShadowClass(Class)} in {@link #createShadowMap()}.</li> * <li>Be sure to use {@code @RunWith(MapzenTestRunner.class)} at the top of your tests.</li> * </ol> */public class MapzenTestRunner extends RobolectricTestRunner {    /**     * List of fully qualified class names backed by custom shadows in the test harness.     */    private static final List<String> CUSTOM_SHADOW_TARGETS =            Collections.unmodifiableList(Arrays.asList(                    "org.oscim.android.MapView",                    "org.oscim.layers.tile.vector.VectorTileLayer",                    "org.oscim.renderer.GLMatrix",                    "org.oscim.renderer.GLShader",                    "org.oscim.renderer.GLState",                    "com.splunk.mint.Mint"            ));    public MapzenTestRunner(Class<?> testClass) throws InitializationError {        super(testClass);    }//    /**//     * Adds custom shadow classes to Robolectric shadow map.//     *///    @Override//    protected ShadowMap createShadowMap() {//        return super.createShadowMap()//                .newBuilder()//                .addShadowClass(ShadowMapView.class)//                .addShadowClass(ShadowVectorTileLayer.class)//                .addShadowClass(ShadowGLMatrix.class)//                .addShadowClass(ShadowGLShader.class)//                .addShadowClass(ShadowGLState.class)//                .addShadowClass(ShadowMint.class)//                .build();//    }////    /**//     * Replaces Robolectric {@link Setup} with {@link MapzenSetup} subclass.//     *///    @Override//    public Setup createSetup() {//        return new MapzenSetup();//    }////    /**//     * Modified Robolectric {@link Setup} that instruments third party classes with custom shadows.//     *///    public class MapzenSetup extends Setup {//        @Override//        public boolean shouldInstrument(ClassInfo classInfo) {//            return CUSTOM_SHADOW_TARGETS.contains(classInfo.getName())//                    || super.shouldInstrument(classInfo);//        }//    }    /**     * Uses custom manifest as workaround to maintain backward compatibility with library projects     * that do not yet include the <code>&lt;application/&gt;</code> tag in AndroidManifest.xml.     * <p />     * See https://github.com/robolectric/robolectric/pull/1309 for more info.     */    @Override    protected AndroidManifest createAppManifest(FsFile manifestFile,            FsFile resDir, FsFile assetsDir) {        AndroidManifest manifest = new MapzenAndroidManifest(manifestFile, resDir, assetsDir);        String packageName = System.getProperty("android.package");        manifest.setPackageName(packageName);        return manifest;    }}
View Code

两个文件加到test project,再跑测试的话这个错误就不会出现了。

 

多项目依赖解决了,但又遇到找不到library project中的native library,也就是.so文件。报错为:

java.lang.UnsatisfiedLinkError: no stlport_shared in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1738)
at java.lang.Runtime.loadLibrary0(Runtime.java:823)
at java.lang.System.loadLibrary(System.java:1028)
at net.sqlcipher.database.SQLiteDatabase.loadLibs(SQLiteDatabase.java:142)
at net.sqlcipher.database.SQLiteDatabase.loadLibs(SQLiteDatabase.java:137)

Google到一个答案(链接),但是好像是1.x版本的,2.x的没人回答,暂时先放这儿了。

 

使用Robolectric对Android应用进行单元测试