使用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!"));
    }

}

为产品项目建了个类似的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 = 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 == 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 = metaData;
    }

    public String getBroadcastReceiverClassName() {
      return broadcastReceiverClassName;
    }

    public List<String> getIntentFilterActions() {
      return intentFilterActions;
    }

    public MetaData getMetaData() {
      return metaData;
    }
  }
}

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;
    }
}

两个文件加到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的没人回答,暂时先放这儿了。

时间: 2024-10-14 20:47:31

使用Robolectric对Android应用进行单元测试的相关文章

Android Day02-Android中单元测试(junit测试)&monkey测试

Android中junit测试有2种实现方式 第1种:一般Android工程的实现方式 1.在清单文件中添加2项内容 首先在AndroidManifest.xml中加入下面红色代码: <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.itcast.action" android:versionCode="1"  android:v

android下的单元测试

android下的单元测试 在AndroidManifest.xml文件中配置以下信息: 在manifest节点下添加: <!-- 指定测试信息和要测试的包 --> <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.jxn.junittest" /> 在application节点下添加: <!

Android学习6&mdash;单元测试的使用

在这里对单元测试的使用,主要介绍两种方法,1.手动添加配置信息,然后编写测试类.2.通过Eclipse创建测试项目 1.手动添加配置信息,然后编写测试类: 待测试的类:/src/com/example/unittest/UnitTestDemo1.java package com.example.unittest; //此类为待测试类 public class UnitTestDemo1 {     public void testing1(String str){         String

Android开发中单元测试的两种方式

Android开发中单元测试的两种方式 一位优秀的程序员也同样不能保证自己的程序没有bug,因此编写合适的测试程序是完全有必要的,这样也会降低程序在后期出现各种奇奇怪怪bug的可能,降低维护成本,未雨绸缪将bug扼杀在摇篮之中. 看到网上有很多依旧用写java单元测试的方式在写android程序的单元测试程序--junit,当然我一直都反感将不合时宜的东西强搬到新的技术应用以获取一席之地的这种做法,不断的应用新的方法提高效率,完善程序才是真理!废话不多说,直接说到今天的重点:Android开发中

Android开发之单元测试(一)

Android开发之单元测试(一) 请尊重他人的劳动成果,转载请注明出处 : Android开发之单元测试(一) http://blog.csdn.net/fengyuzhengfan/article/details/40209995 在实际开发中,开发android软件的过程需要不断地进行测试.进行Android单元测试是正规Android开发的必经步骤.单元测试可以嵌入到项目中:也可以作为一个单独的项目针对某个具体项目进行测试. 1.   Android单元测试框架的层次结构 从上图可以看出

Android(java)学习笔记165:Android下编写单元测试代码(Junit Test)

编写android应用的时候,往往我们需要编写一些业务逻辑实现类,但是我们可能不能明确这个业务逻辑是否可以成功实现,特别是逻辑代码体十分巨大的时候,我们不可能一行一行检查自己的代码,为了解决这样的问题就出现了: Android下编写单元测试代码-----Junit Test       测试逻辑是:在Eclipse我们待测试项目中编写测试代码,然后运行测试代码,系统会把代码布署到模拟器或者真机中,代码运行之后,会反馈测试结果给Eclipse,用户就知道业务逻辑类是否可以成功实现. 首先我们明确A

Android如何进行单元测试

Menifest.xml中加入: <application>中加入: <uses-library android:name="android.test.runner" /> <application>外面加入: <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> <instrumentation android:nam

配置同时使用PowerMock和Robolectric对Android进行单元测试

Robolectric官网上给了一个配置教程,但是我使用它的方法进行配置,发现使用Mockito.spy函数的时候会出现Exception. 后来在PowerMock官网上找到了另外一个教程,里面说使用PowerMockRule是不靠谱的,要使用PowerMock 1.6.0引入的新的@PowerMockRunnerDelegate annotation来进行配置 具体配置文件如下: module里面的build.gradle添加依赖: dependencies { ...... testCom

【Android开发经验】使用Android Studio进行单元测试

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992 Android Studio已经到了1.2版本,国内的开发者基本也在从Eclipse向Android Studio进行转变,对于Android开发者,以后必将是Android Studio的天下. 昨天在完善煎蛋项目的时候,需要进行单元测试,在Eclipse环境中进行是很简单的,但是在Android Studio环境中进行单元测试,我还没有尝试过,在国内找了很多资料,大都是人云亦云,也没有测试成功,然后在