Robolectric and Android Studio 1.1.0 and library testing - android

I am having trouble getting my Robolectric unit tests to run under the experimental AS 1.1 unit testing variant. The error I am getting is shown below;
java.lang.NoClassDefFoundError: com/my/app/R$string
at com.my.app.MoneyFormatter.formatDealMessage(MoneyFormatter.java:63)
...
Caused by: java.lang.ClassNotFoundException: com.my.app.R$string
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
at org.robolectric.bytecode.AsmInstrumentingClassLoader.loadClass(AsmInstrumentingClassLoader.java:100)
... 46 more
I retrieve this error by executing from the command line at the root (app-name in Snippet1) the command;
./gradlew core:library:test
This approach works in Android Studio 1.1 without unit testing enabled and using the android gradle tooling at v1.0 (com.android.tools.build:gradle:1.0.0).
Project configuration
My project has a complex structure as prescribed below;
app-name
|
|->app
|->src
|-> androidTest // Espresso helper classes reside here
|-> androidTestFlavour1 // Espresso int tests relating to 'flavour 1'
|-> androidTestFlavour2 // Espresso int tests relating to 'flavour 2'
|-> flavour1
|-> flavour2
|-> main
+ AndroidManifest.xml // Manifest for apps
|-> testFlavour1 // Robolectric unit tests for flavour 1
|-> testFlavour2 // Robolectric unit tests for flavour 2
|->core
|->library
|-> src
|-> main // Library code resides here
+ AndroidManifest.xml
|-> test // Robolectric unit tests for library
+gradle
+gradlew
+settings.gradle
+local.properties
Snippet 1: Project schematic
I have followed various pieces of advice from Pivotal Labs themselves and numerous other home-brew alternatives with no success.
What is the error saying and what do I need to change to put it right?
Update:
So having inspected the classpath being used to run the tests I noted the following library paths;
/workspace/app/core/lib/build/intermediates/bundles/debug/classes.jar
/workspace/app/core/lib/build/intermediates/classes/test/debug
/workspace/app/core/lib/build/intermediates/dependency-cache/test/debug
Neither of these directories contain the generated R$string.class file hence the exception. The directory containing the generated 'R' files include the debug and release build types of the application. So;
/workspace/app/core/lib/build/intermediates/classes/debug
/workspace/app/core/lib/build/intermediates/classes/release
This indicates that a part of the build process is missing adding the "debug" or "release" build type resources. This also backs-up the behaviour whereby pure-Java test cases (that only rely on the classes.jar) work fine.

OK! I've got it working. I have added the following line to the library build gradle to forcibly add the generated "R" files onto the test classpath;
sourceSets {
test.java.srcDirs += "build/generated/source/r/debug"
}
With this I have reached the nirvana of Robolectric tests with;
./gradlew core:lib:test
and Espresso 2.0-based integration with;
./gradlew connectedAndroidTest
Stop the clock for unit and integration test support in the IDE.... oh, what's that? 7 YEARS?! No love lost. Kudos to this answer that showed me how to forcibly add stuff to the test classpath.

Related

Android: after Gradle update to 6.7.1 ClassLoader in JUnit test no longer lists all resources

I need to iterate over specific classes from main package in my android unit test, to check some of their properties.
For this I use standard approach, using ClassLoader:
val classLoader = Thread.currentThread().contextClassLoader
val resources: Enumeration<URL> = classLoader.getResources("com/models/package")
assert(resources.hasMoreElements()) // Fails from CL, works in AS
Before the Gradle update (had Gradle 5.6.4) that worked. Now the behaviour is as follows: it works when test is run from Android Studio, but fails (returns empty enumeration) when run from command line with gradlew.
I wonder what might be the difference in this respect between the two Gradle versions? And why it still works when run from Studio?
Some considerations and things I have tried:
Referencing these classes in unit test works ok, and also classLoader.findClass("com.models.package.MyModel") and
classLoader.loadClass("com.models.package.MyModel") from unit test is working. But even after that classLoader.getResources("com/models/package") returns empty enumeration.
Using other references to ClassLoader, like MyModel::class.java.classLoader and ClassLoader.getSystemClassLoader() didn't make any difference.
Gradle build from command line contains the warning "OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended", but as far as I can tell it's not connected to my issue.
If I put some of the classes from 'com/models/package' to the unit test /test folder, they are getting returned in enumeration.
This might be connected with some new optimisation setting that makes ClassLoaders omit registering some of the classes, in different root directories, but as it still works in AS there might be some setting to turn this optimisation off in a command line build also?
Thank you for any suggestions on this.
In Gradle 6.7.1 I had to include the directory with the code to the test sourceSets. Afterwards the classloader from junit started to see the classes and return them in Enumeration.
sourceSets {
test {
java.srcDirs += ['src/main']
}
}

Android Robolectric Exception

I am facing a problem with Robolectric library. I am writing unit tests using Robolectric, it's working locally but when I merge my code, it crashes on pipeline(remotely).
I am using 'org.robolectric:robolectric:4.0.2'
It fails by only adding this line to my test class: #RunWith(RobolectricTestRunner.class)
And the exception is:
FAILED
org.apache.tools.ant.BuildException
Caused by: org.apache.maven.artifact.resolver.MultipleArtifactsNotFoundException
I had the same issue: Robolectric was working fine locally, but once pushed to Jenkins the gradle task to execute the tests fails.
You can execute the gradle task to run the tests with -i -d flags to see more debug output.
./gradlew -i -d test
For me this revealed that Jenkins was unable to download Robolectric dependencies:
13:58:43 13:58:42.904 [DEBUG] [TestEventLogger] com.my.package.Test > my_test_case STANDARD_ERROR
13:58:43 13:58:42.904 [DEBUG] [TestEventLogger] Downloading: org/robolectric/android-all/9-robolectric-4913185-2/android-all-9-robolectric-4913185-2.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
I could solve it by telling gradle to use our corporate proxy when running on Jenkins. One way to achieve this would be to add the following to your gradle.properties:
systemProp.http.proxyHost=http://proxy.host
systemProp.http.proxyPort=3128
systemProp.https.proxyHost=http://proxy.host
systemProp.https.proxyPort=3128
---- EDIT ----
Actually I found a cleaner solution for my use case then configuring a proxy: Robolectric offers a way to configure the repository it uses during runtime (see http://robolectric.org/configuring/). This way I was able to tell it to use our corporate repository.
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}
In my case the issue was (./gradlew -i -d testDebug printed it):
Caused by:
org.apache.maven.artifact.resolver.MultipleArtifactsNotFoundException: Missing:
----------
1) org.robolectric:android-all:jar:10-robolectric-5803371
...
Robolectric tried to fetch a dependency from the remote repository, but it couldn't.
As the output suggests, I have:
Manually downloaded org.robolectric:android-all:jar:10-robolectric-5803371 jar from the Maven repository.
Located it in /home/user/jars
Installed Maven on my machine: sudo apt install maven
Linked the jar with Robolectic: mvn install:install-file -DgroupId=org.robolectric -DartifactId=android-all -Dversion=10-robolectric-5803371 -Dpackaging=jar -Dfile=/home/user/jars/android-all-10-robolectric-5803371.jar
Now ./gradlew testDebug works fine.
Also check the issue on Github, maybe there will be some more useful information for you.
As #Christian.D said, Robolectric tries to download dependencies from external repository, but we need that it uses internal one. Below the fix:
1) Create a custom RobolectricTestRunner:
public class CustomRobolectricRunner extends RobolectricTestRunner {
public CustomRobolectricRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
static {
RoboSettings.setMavenRepositoryId("my-nexus");
RoboSettings.setMavenRepositoryUrl("your_custom_url");
}
}
2) Annotate your test class:
#Config(manifest = Config.NONE)
#RunWith(CustomRobolectricRunner.class)
Good coding!

Android Studio Gradle - JavaExec classpath configuration - Java Reflection - class access

Im currently working on an android project where i have to process .java-files to possibly generate another .java-files which should then be compiled and packed into the .apk-file.
Lets assume i have 2 files which will be processed by my library, FILE_A.java and FILE_B.java.
Now i need to access these files within my library via reflection, e.g. with:
Class.forName("com.test.entities.FILE_A");
Class.forName("com.test.entities.FILE_B");
The problem is that i'm not able to access the class files, i think because of the missing classpath configuration. Currently i use this task to call my .jar-file:
task (mytask, type: org.gradle.api.tasks.JavaExec) {
classpath(files('libs/myjar.jar'))
main('com.test.TestMain')
}
preBuild.dependsOn mytask
I found some ressources on the web, but they all don't work.
I tried to add the following to the classpath:
sourceSets.main.runtimeClasspath (main is unknown)
android.sourceSets.main.runtimeClasspath (runtimeClasspath is unkown).
So how can i access the class files in my library?
Try this
task execute(dependsOn: ['compileReleaseJavaWithJavac'], type:JavaExec) {
main = 'com.geniml.Main'
classpath(files('build/intermediates/classes/release',"${android.getSdkDirectory().getAbsolutePath() + '/platforms/' + android.compileSdkVersion + '/android.jar'}"))
}
android gralde is 1.5.
As to build dir, you can use rootProject.getBuildDir(). But normally build dir is a convention. A static way is ok.

Method setUp in android.test.AndroidTestCase not mocked

I'm trying to come to terms with the new unit test feature of Android Studio.
I've followed the instructions on http://tools.android.com/tech-docs/unit-testing-support. The description there explicitly mentions the 'Method ... not mocked' error and suggests to put the following into the build.gradle:
android {
// ...
testOptions {
unitTests.returnDefaultValues = true
}
}
This works in so far as the tests run when started from the command line with
gradlew test --continue
but not when I run the test class from Android Studio with rightclick -> run. This way, I get the same error again:
java.lang.RuntimeException: Method setUp in android.test.AndroidTestCase not mocked. See https://sites.google.com/a/android.com/tools/tech-docs/unit-testing-support for details.
at android.test.AndroidTestCase.setUp(AndroidTestCase.java)
at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:86)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:211)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:67)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Any ideas on how to solve this?
EDIT: The content of the test class doesn't really matter because the setUp of the test fails, I tried with the most simple class:
public class ContactFormToolTest extends AndroidTestCase {
public void testSOmething(){
assertEquals(false, true);
}
}
Also tried overriding setUp, makes no difference.
From: https://sites.google.com/a/android.com/tools/tech-docs/unit-testing-support#TOC-Method-...-not-mocked.-
The android.jar file that is used to run unit tests does not contain
any actual code - that is provided by the Android system image on real
devices. Instead, all methods throw exceptions (by default). This is
to make sure your unit tests only test your code and do not depend on
any particular behaviour of the Android platform (that you have not
explicitly mocked e.g. using Mockito). If that proves problematic, you
can add the snippet below to your build.gradle to change this
behavior:
android {
// ...
testOptions {
unitTests.returnDefaultValues = true
}
}
The new Unit Tests feature in Android Studio fakes the entire Android SDK so that you can run fast, Java-only tests, without needing to install your application on an Android device (this is similar to Robolectric). The general idea is that you mock all the responses from the Android SDK calls.
AndroidTestCase is used to run a test with the real Android SDK.
So, your issue is that you are trying to run an AndroidTestCase that depends on the Android SDK, but your test runner is launching the Unit Tests environment, which uses a fake Android SDK instead of a real one.
You need to choose one approach. If you want a pure unit test, then you probably should use a JUnit 4 test class instead of an AndroidTestCase. More instructions here:
https://developer.android.com/training/testing/unit-testing/local-unit-tests.html#build
As of SDK version 24, AndroidTestCase is deprecated
This class was deprecated in API level 24.
Use InstrumentationRegistry instead. New tests should be written using
the Android Testing Support Library.
You are supposed to use the Espresso framework for UI testing. There is a tutorial.

Is there anyway to prepend a jar to the unmanagedClasspath in sbt

I am using the android-sbt-plugin with the sbt, and I would like to add an unmanaged jar to the test classpath. The reason being android.jar contains stub functions for the org.json libraries and results in exceptions being thrown for unit tests. This is what I am doing
unmanagedClasspath in Test <+= (baseDirectory) map { base =>
Attributed.blank(base/"test-libs"/"json.jar")
}
Because of the order of the jars this file is ignored during when i run the test command within the sbt. If I type the command the order clearly shows the android.jar as the first jar
show test:unmanaged-classpath
[info] ArrayBuffer(Attributed(/home/rohit/Projects/android-sdk-linux/platforms/android- 17/android.jar), Attributed(/home/rohit/Projects/barfrendz/trunk/src/buzze/test-libs/json.jar))
If I create a lib folder and let sbt pick up the json jar the order is reversed the tests now run, but I can no longer create an android package due to conflicts with the org.json namespace in android.jar. Here is the exception
[error] (Buzze/android:proguard) java.io.IOException: Can't read [/home/rohit/Projects/barfrendz/trunk/src/buzze/lib/json.jar(;;;;!META-INF/MANIFEST.MF,!**/R.class,!**/R$*.class,!**/TR.class,!**/TR$.class,!**/library.properties)] (Can't process class [org/json/CDL.class] (Unsupported version number [51.0] for class format))
Is there anyway I can change the order of the jars in the classpath for the unit tests?
Instead of using <+=, use <<=, get unmanagedClasspath itself as a dependency, and then modify it as desired. The documentation has such an example with resolvers:
resolvers <<= resolvers {rs =>
val localMaven = "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
localMaven +: rs
}
This way, localMaven ends up first in resolvers.
According to the API docs, the unmanagedClasspath is a Task of type Classpath. Note that when you use that syntax, you are changing the Classpath, not the Task.
The API doc for the classpath is here -- it's a type, and it points to Seq[Attributed[File]], so you can manipulate it with any Seq command. I tried out the snippet here and it works:
$ cat build.sbt
unmanagedClasspath in Test <<= (unmanagedClasspath in Test, baseDirectory) map { (uc, base) =>
Attributed.blank(base/"test-libs"/"json.jar") +: uc
}
Daniel#DANIEL-PC /c/scala/Programas/sbtTest
$ sbt
[info] Set current project to default-60c6f9 (in build file:/C:/scala/Programas/sbtTest/)
> show test:unmanaged-classpath
[info] ArrayBuffer(Attributed(C:\scala\Programas\sbtTest\test-libs\json.jar))
[success] Total time: 0 s, completed 30/08/2013 13:32:42
>
Maybe overriding the unmanagedJars instead of the unmanagedClasspath would allow you to do this:
http://www.scala-sbt.org/0.12.3/docs/Detailed-Topics/Library-Management.html

Categories

Resources