├── .gitignore ├── settings.gradle ├── gradle.properties ├── src ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── de │ │ └── lemona │ │ └── android │ │ └── testng │ │ ├── AndroidTestNGModule.java │ │ ├── AndroidTestNGSupport.java │ │ ├── TestNGArgs.java │ │ ├── TestNGCoverageListener.java │ │ ├── TestNGLogger.java │ │ ├── TestNGRunner.java │ │ └── TestNGListener.java └── androidTest │ ├── assets │ └── testng.xml │ └── java │ └── de │ └── lemona │ └── android │ └── testng │ ├── others │ └── TestFromXMLFile.java │ └── test │ ├── BeforeHookTest.java │ ├── PrioritiesTest.java │ ├── DataProviderTest.java │ ├── FailingTest.java │ ├── GuiceInjectionTest.java │ ├── LivecycleTestVerifier.java │ ├── AndroidComponentsTest.java │ ├── TestNGArgsTest.java │ └── LifecycleTest.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── Jenkinsfile ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | build 3 | project.properties 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'android-testng' 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group = de.lemona.android 2 | version = 1.1 3 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonadeLabInc/android-testng/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 31 15:17:18 JST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /src/androidTest/assets/testng.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/others/TestFromXMLFile.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.others; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | public class TestFromXMLFile { 6 | 7 | @Test 8 | public void testFromXMLFile() { 9 | // Nothing to do, this should just run... 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/BeforeHookTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.BeforeClass; 5 | import org.testng.annotations.Test; 6 | 7 | import java.util.concurrent.atomic.AtomicReference; 8 | 9 | public class BeforeHookTest { 10 | 11 | private AtomicReference ref = new AtomicReference<>(null); 12 | @BeforeClass 13 | public void beforeClass() { 14 | // processing before test 15 | ref.set(new Object()); 16 | } 17 | 18 | @Test 19 | public void verifyBeforeWorked() { 20 | Assert.assertNotNull(ref.get(), "This should never be null"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/PrioritiesTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Test; 5 | 6 | public class PrioritiesTest { 7 | 8 | private boolean firstCalled = false; 9 | private boolean secondCalled = false; 10 | 11 | @Test(priority=1) 12 | public void myFirstTest() { 13 | firstCalled = true; 14 | } 15 | 16 | @Test(priority=2) 17 | public void mySecondTest() { 18 | secondCalled = true; 19 | } 20 | 21 | @Test(priority=3) 22 | public void verifySimpleTest() { 23 | Assert.assertTrue(firstCalled, "SimpleTest.myFirstTest() not called"); 24 | Assert.assertTrue(secondCalled, "SimpleTest.mySecondTest() not called"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/DataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.DataProvider; 5 | import org.testng.annotations.Test; 6 | 7 | public class DataProviderTest { 8 | 9 | @DataProvider(name="simpleProvider") 10 | public Object[][] provideData() { 11 | return new Object[][] { 12 | new Object[] { "foo" }, 13 | new Object[] { "bar" }, 14 | new Object[] { "baz" }, 15 | }; 16 | } 17 | 18 | @Test(dataProvider="simpleProvider") 19 | public void testWithData(String data) { 20 | Assert.assertNotNull(data); 21 | if ("foo".equals(data)) return; 22 | if ("bar".equals(data)) return; 23 | if ("baz".equals(data)) return; 24 | Assert.fail("No \"foo\", \"bar\" or \"baz\" => \"" + data + "\""); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/FailingTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Test; 5 | 6 | public class FailingTest { 7 | 8 | @Test 9 | public void failingTest() { 10 | try { 11 | Assert.fail("This is an expected failure for an @Test method"); 12 | throw new RuntimeException("This should not be thrown."); // if assertion is failed, NullPointerException caused a failure of test. 13 | } catch (AssertionError ae) { 14 | // success 15 | } 16 | } 17 | 18 | @Test(expectedExceptions=NullPointerException.class) 19 | public void expectedFailingTest() { 20 | throw new NullPointerException("This is expected"); 21 | } 22 | 23 | @Test(enabled=false) 24 | public void shouldBeSkippedAgain() { 25 | Assert.fail("This test should be skipped"); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/GuiceInjectionTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Guice; 5 | import org.testng.annotations.Test; 6 | 7 | import android.app.Instrumentation; 8 | import android.content.Context; 9 | 10 | import com.google.inject.Inject; 11 | 12 | import de.lemona.android.testng.AndroidTestNGModule; 13 | 14 | @Guice(modules=AndroidTestNGModule.class) 15 | public class GuiceInjectionTest { 16 | 17 | private final Context context; 18 | private final Instrumentation instrumentation; 19 | 20 | @Inject 21 | private GuiceInjectionTest(Context context, Instrumentation instrumentation) { 22 | this.context = context; 23 | this.instrumentation = instrumentation; 24 | } 25 | 26 | @Test 27 | public void testWasConstructed() { 28 | Assert.assertNotNull(this.context, "Null context"); 29 | Assert.assertNotNull(this.instrumentation, "Null instrumentation"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/LivecycleTestVerifier.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import static de.lemona.android.testng.test.LifecycleTest.EVENTS; 4 | 5 | import org.testng.Assert; 6 | import org.testng.annotations.Test; 7 | 8 | public class LivecycleTestVerifier { 9 | 10 | @Test(dependsOnGroups="Lifecycle") 11 | public void verifyLifecycle() { 12 | Assert.assertEquals(EVENTS.size(), 12, LifecycleTest.EVENTS.toString()); 13 | Assert.assertEquals(EVENTS.get( 0), "BEFORE_SUITE"); 14 | Assert.assertEquals(EVENTS.get( 1), "BEFORE_TEST"); 15 | Assert.assertEquals(EVENTS.get( 2), "BEFORE_CLASS"); 16 | Assert.assertEquals(EVENTS.get( 3), "BEFORE_GROUPS"); 17 | Assert.assertEquals(EVENTS.get( 4), "BEFORE_METHOD"); 18 | Assert.assertEquals(EVENTS.get( 5), "FIRST_TEST"); 19 | Assert.assertEquals(EVENTS.get( 6), "AFTER_METHOD"); 20 | Assert.assertEquals(EVENTS.get( 7), "BEFORE_METHOD"); 21 | Assert.assertEquals(EVENTS.get( 8), "SECOND_TEST"); 22 | Assert.assertEquals(EVENTS.get( 9), "AFTER_METHOD"); 23 | Assert.assertEquals(EVENTS.get(10), "AFTER_GROUPS"); 24 | Assert.assertEquals(EVENTS.get(11), "AFTER_CLASS"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/AndroidTestNGModule.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Instrumentation; 4 | import android.content.Context; 5 | 6 | import com.google.inject.AbstractModule; 7 | import com.google.inject.Binder; 8 | import com.google.inject.Module; 9 | 10 | /** 11 | * A basic {@link Module} which can be used to inject {@link Context} and 12 | * {@link Instrumentation} with Google Guice. 13 | *

14 | * For example: 15 | *

16 |  * {@literal @}Guice(modules=AndroidTestNGModule.class)
17 |  *  public class GuiceInjectionTest {
18 |  *    {@literal @}Inject private InjectedTest(Context context) {
19 |  *       ...
20 |  *    }
21 |  *  }
22 |  * 
23 | */ 24 | public class AndroidTestNGModule extends AbstractModule { 25 | 26 | /** 27 | * Create a new {@link Module} binding {@link Context} and 28 | * {@link Instrumentation} instances. 29 | */ 30 | public AndroidTestNGModule() { 31 | // Nothing to do, javadoc only 32 | } 33 | 34 | /** 35 | * Bind the {@link Context} and {@link Instrumentation} instances. 36 | */ 37 | @Override 38 | public void configure() { 39 | Binder binder = binder(); 40 | binder.bind(Context.class).toInstance(AndroidTestNGSupport.getContext()); 41 | binder.bind(Instrumentation.class).toInstance(AndroidTestNGSupport.getInstrumentation()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | def versionFromProperties() { 2 | node { 3 | checkout scm 4 | def props = readProperties(file: 'gradle.properties') 5 | return props.version 6 | } 7 | } 8 | 9 | @Library('leomo-android') _ 10 | pipeline { 11 | agent any 12 | environment { 13 | API_LEVEL = 'all' 14 | BUILD_ID = leomoSetupBuildId() 15 | S3_ACCESS_KEY = credentials('leomo-library-s3-access-key') 16 | S3_SECRET_KEY = credentials('leomo-library-s3-secret-key') 17 | S3_REPOSITORY = credentials('leomo-library-s3-repository') 18 | VERSION = versionFromProperties() 19 | } 20 | stages { 21 | stage('Deploy') { 22 | when { 23 | anyOf { 24 | branch 'master' 25 | branch 'hotfix/*' 26 | branch 'release/*' 27 | changeRequest branch: 'hotfix/*', comparator: 'GLOB' 28 | changeRequest branch: 'release/*', comparator: 'GLOB' 29 | } 30 | } 31 | steps { 32 | leomoAndroidBuild(apiLevel: env.API_LEVEL, { 33 | sh "./gradlew -PbuildNumber=${env.BUILD_ID} clean publish uploadS3 -x javadocRelease -Ps3.accessKey=${env.S3_ACCESS_KEY} -Ps3.secretKey=${env.S3_SECRET_KEY}" 34 | }) 35 | leomoTag "${env.VERSION}.${env.BUILD_ID}", "Jenkins Build ${env.BUILD_DISPLAY_NAME}\nSee ${env.BUILD_URL}" 36 | } 37 | } 38 | } 39 | post { 40 | always { 41 | leomoBuildFinished() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/AndroidComponentsTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import static de.lemona.android.testng.AndroidTestNGSupport.getContext; 4 | import static de.lemona.android.testng.AndroidTestNGSupport.getInstrumentation; 5 | 6 | import org.testng.Assert; 7 | import org.testng.annotations.BeforeClass; 8 | import org.testng.annotations.BeforeGroups; 9 | import org.testng.annotations.BeforeSuite; 10 | import org.testng.annotations.BeforeTest; 11 | import org.testng.annotations.Test; 12 | 13 | public class AndroidComponentsTest { 14 | 15 | @BeforeSuite 16 | public void beforeSuite() { 17 | Assert.assertNotNull(getContext(), "Non-null context in @BeforeSuite"); 18 | Assert.assertNotNull(getInstrumentation(), "Non-null instrumentation in @BeforeSuite"); 19 | } 20 | 21 | @BeforeGroups(groups="Components") 22 | public void beforeGroups() { 23 | Assert.assertNotNull(getContext(), "Null context in @BeforeGroups"); 24 | Assert.assertNotNull(getInstrumentation(), "Null instrumentation in @BeforeGroups"); 25 | } 26 | 27 | @BeforeClass 28 | public void beforeClass() { 29 | Assert.assertNotNull(getContext(), "Null context in @BeforeClass"); 30 | Assert.assertNotNull(getInstrumentation(), "Null instrumentation in @BeforeClass"); 31 | } 32 | 33 | @BeforeTest 34 | public void beforeTest() { 35 | Assert.assertNotNull(getContext(), "Null context in @BeforeTest"); 36 | Assert.assertNotNull(getInstrumentation(), "Null instrumentation in @BeforeTest"); 37 | } 38 | 39 | @Test(groups="Components") 40 | public void testComponents() { 41 | Assert.assertNotNull(getContext(), "Null context in @Test"); 42 | Assert.assertNotNull(getInstrumentation(), "Null instrumentation in @Test"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/AndroidTestNGSupport.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Instrumentation; 4 | import android.content.Context; 5 | 6 | /** 7 | * An utility class suitable for accessing the {@link Instrumentation} and 8 | * {@link Context} instances from tests. 9 | *

10 | * NOTE: this is a bit of a hack (static), but it's the easiest way to 11 | * implement it without getting lost in TestNG's hooks. 12 | */ 13 | public abstract class AndroidTestNGSupport { 14 | 15 | private AndroidTestNGSupport() { 16 | throw new IllegalStateException("Do not construct"); 17 | } 18 | 19 | /* ====================================================================== */ 20 | 21 | private static Instrumentation instrumentation = null; 22 | 23 | static final void injectInstrumentation(Instrumentation instrumentation) { 24 | AndroidTestNGSupport.instrumentation = instrumentation; 25 | } 26 | 27 | /* ====================================================================== */ 28 | 29 | /** 30 | * Return the {@link Context} associated with the tests. 31 | * 32 | * @return A non-null {@link Context} instance. 33 | * @throws IllegalStateException If no instance was available. 34 | */ 35 | public static final Context getContext() { 36 | if (instrumentation == null) throw new IllegalStateException("No Android instrumentation available"); 37 | final Context context = instrumentation.getTargetContext(); 38 | if (context == null) throw new IllegalStateException("No android context available"); 39 | return context; 40 | } 41 | 42 | /** 43 | * Return the {@link Instrumentation} associated with the tests. 44 | * 45 | * @return A non-null {@link Instrumentation} instance. 46 | * @throws IllegalStateException If no instance was available. 47 | */ 48 | public static final Instrumentation getInstrumentation() { 49 | return instrumentation; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/TestNGArgsTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.Bundle; 5 | 6 | import com.google.inject.Inject; 7 | 8 | import org.testng.Assert; 9 | import org.testng.annotations.Guice; 10 | import org.testng.annotations.Test; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import de.lemona.android.testng.AndroidTestNGModule; 16 | import de.lemona.android.testng.TestNGArgs; 17 | 18 | /** 19 | * 20 | */ 21 | @Guice(modules=AndroidTestNGModule.class) 22 | public class TestNGArgsTest { 23 | 24 | private final Instrumentation instrumentation; 25 | 26 | @Inject 27 | public TestNGArgsTest(Instrumentation instrumentation) { 28 | this.instrumentation = instrumentation; 29 | } 30 | 31 | @Test 32 | public void testParseArguments() { 33 | Map args = new HashMap(){{ 34 | put(TestNGArgs.ARGUMENT_DEBUG,"true"); 35 | put(TestNGArgs.ARGUMENT_COVERAGE,"true"); 36 | put(TestNGArgs.ARGUMENT_COVERAGE_PATH, "somewhere"); 37 | }}; 38 | 39 | Bundle bundle = getTestArguments(args); 40 | 41 | TestNGArgs.Builder builder = new TestNGArgs.Builder(instrumentation); 42 | builder = builder.fromBundle(bundle); 43 | TestNGArgs testNGArgs = builder.build(); 44 | Assert.assertTrue(testNGArgs.debug, "argument should be true"); 45 | Assert.assertTrue(testNGArgs.codeCoverage, "argument should be true"); 46 | Assert.assertEquals("somewhere", testNGArgs.codeCoveragePath, "argument should be received"); 47 | 48 | args.put(TestNGArgs.ARGUMENT_DEBUG, "false"); 49 | args.put(TestNGArgs.ARGUMENT_COVERAGE,"false"); 50 | args.remove(TestNGArgs.ARGUMENT_COVERAGE_PATH); 51 | 52 | bundle = getTestArguments(args); 53 | builder = builder.fromBundle(bundle); 54 | TestNGArgs testNGArgs2 = builder.build(); 55 | Assert.assertFalse(testNGArgs2.debug, "argument should be false"); 56 | Assert.assertFalse(testNGArgs2.codeCoverage,"argument should be false"); 57 | Assert.assertNull(testNGArgs2.codeCoveragePath, "argument should be null"); 58 | } 59 | 60 | 61 | private Bundle getTestArguments(Map args) { 62 | Bundle bundle = new Bundle(); 63 | for (String key : args.keySet()) { 64 | bundle.putString(key, args.get(key)); 65 | } 66 | return bundle; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/androidTest/java/de/lemona/android/testng/test/LifecycleTest.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.testng.annotations.AfterClass; 7 | import org.testng.annotations.AfterGroups; 8 | import org.testng.annotations.AfterMethod; 9 | import org.testng.annotations.BeforeClass; 10 | import org.testng.annotations.BeforeGroups; 11 | import org.testng.annotations.BeforeMethod; 12 | import org.testng.annotations.BeforeSuite; 13 | import org.testng.annotations.BeforeTest; 14 | import org.testng.annotations.Test; 15 | 16 | @Test(groups="Lifecycle") 17 | public class LifecycleTest { 18 | 19 | static final List EVENTS = new ArrayList<>(); 20 | 21 | /* ====================================================================== */ 22 | 23 | @BeforeSuite 24 | public void beforeSuite() { 25 | EVENTS.add("BEFORE_SUITE"); 26 | } 27 | 28 | // @AfterSuite 29 | // public void afterSuite() { 30 | // EVENTS.add("AFTER_SUITE"); 31 | // } 32 | 33 | /* ====================================================================== */ 34 | 35 | @BeforeGroups(groups="Lifecycle") 36 | public void beforeGroups() { 37 | EVENTS.add("BEFORE_GROUPS"); 38 | } 39 | 40 | @AfterGroups(groups="Lifecycle") 41 | public void afterGroups() { 42 | EVENTS.add("AFTER_GROUPS"); 43 | } 44 | 45 | /* ====================================================================== */ 46 | 47 | @BeforeClass 48 | public void beforeClass() { 49 | EVENTS.add("BEFORE_CLASS"); 50 | } 51 | 52 | 53 | @AfterClass 54 | public void afterClass() { 55 | EVENTS.add("AFTER_CLASS"); 56 | } 57 | 58 | /* ====================================================================== */ 59 | 60 | @BeforeTest 61 | public void beforeTest() { 62 | EVENTS.add("BEFORE_TEST"); 63 | } 64 | 65 | // @AfterTest 66 | // public void afterTest() { 67 | // EVENTS.add("AFTER_TEST"); 68 | // } 69 | 70 | /* ====================================================================== */ 71 | 72 | @BeforeMethod 73 | public void beforeMethod() { 74 | EVENTS.add("BEFORE_METHOD"); 75 | } 76 | 77 | @AfterMethod 78 | public void afterMethod() { 79 | EVENTS.add("AFTER_METHOD"); 80 | } 81 | 82 | /* ====================================================================== */ 83 | 84 | @Test 85 | public void firstTest() { 86 | EVENTS.add("FIRST_TEST"); 87 | } 88 | 89 | @Test(dependsOnMethods="firstTest") 90 | public void secondTest() { 91 | EVENTS.add("SECOND_TEST"); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/TestNGArgs.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import java.io.File; 8 | 9 | /** 10 | * A TestNG runner arguments. 11 | */ 12 | public class TestNGArgs { 13 | 14 | /* ARGUMENT KEYS */ 15 | public static final String ARGUMENT_DEBUG = "debug"; 16 | public static final String ARGUMENT_COVERAGE = "coverage"; 17 | public static final String ARGUMENT_COVERAGE_PATH = "coverageFile"; 18 | 19 | /* Default Values */ 20 | 21 | private static final String DEFAULT_COVERAGE_FILE_NAME = "coverage.ec"; 22 | 23 | 24 | public final boolean debug; 25 | public final boolean codeCoverage; 26 | public final String codeCoveragePath; 27 | 28 | private TestNGArgs(Builder builder) { 29 | this.debug = builder.debug; 30 | this.codeCoverage = builder.codeCoverage; 31 | this.codeCoveragePath = builder.codeCoveragePath; 32 | 33 | Log.d(TestNGLogger.TAG, this.toString()); 34 | } 35 | 36 | public static class Builder { 37 | private final Instrumentation instrumentation; 38 | private boolean debug = false; 39 | private boolean codeCoverage = false; 40 | private String codeCoveragePath = null; 41 | 42 | public Builder(Instrumentation instrumentation) { 43 | this.instrumentation = instrumentation; 44 | } 45 | 46 | public Builder fromBundle(Bundle bundle) { 47 | debug = parseBoolean(bundle.getString(ARGUMENT_DEBUG)); 48 | codeCoverage = parseBoolean(bundle.getString(ARGUMENT_COVERAGE)); 49 | codeCoveragePath = bundle.getString(ARGUMENT_COVERAGE_PATH); 50 | if (codeCoverage && codeCoveragePath == null) { 51 | codeCoveragePath = instrumentation.getTargetContext().getFilesDir().getAbsolutePath() + 52 | File.separator + DEFAULT_COVERAGE_FILE_NAME; 53 | } 54 | 55 | return this; 56 | } 57 | 58 | public TestNGArgs build() { 59 | return new TestNGArgs(this); 60 | } 61 | 62 | /** 63 | * Parse boolean value from a String 64 | * 65 | * @return the boolean value, false on null input 66 | */ 67 | private boolean parseBoolean(String booleanValue) { 68 | return booleanValue != null && Boolean.parseBoolean(booleanValue); 69 | } 70 | 71 | } 72 | 73 | public String toString() { 74 | return "[" + TestNGArgs.class.getSimpleName() + "]\n" + 75 | "\tdebug = " + debug + "\n" + 76 | "\tcodeCoverage = " + codeCoverage + "\n" + 77 | "\tcodeCoveragePath = " + codeCoveragePath; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/TestNGCoverageListener.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import org.testng.ITestContext; 8 | import org.testng.ITestListener; 9 | import org.testng.ITestResult; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.lang.reflect.Method; 13 | 14 | /** 15 | * A TestNG {@link ITestListener} that supports Emma CodeCoverage. 16 | */ 17 | public class TestNGCoverageListener implements ITestListener { 18 | 19 | private static final String REPORT_KEY_COVERAGE_PATH = "coverageFilePath"; 20 | 21 | private final Instrumentation instrumentation; 22 | private final Bundle bundle; 23 | private final String mCoverageFilePath; 24 | private boolean isFailed = false; 25 | private boolean isSkipped = false; 26 | 27 | /** 28 | * Create a new {@link ITestListener} instance to generates coverage output. 29 | * 30 | * @param instrumentation TThe {@link Instrumentation} running tests. 31 | */ 32 | protected TestNGCoverageListener(Instrumentation instrumentation, String coverageFilePath) { 33 | this.instrumentation = instrumentation; 34 | this.bundle = new Bundle(); 35 | this.mCoverageFilePath = coverageFilePath; 36 | } 37 | 38 | @Override 39 | public void onTestStart(ITestResult result) { 40 | } 41 | 42 | @Override 43 | public void onTestSuccess(ITestResult result) { 44 | } 45 | 46 | @Override 47 | public void onTestFailure(ITestResult result) { 48 | this.isFailed = true; 49 | } 50 | 51 | @Override 52 | public void onTestSkipped(ITestResult result) { 53 | this.isSkipped = true; 54 | } 55 | 56 | @Override 57 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) { 58 | } 59 | 60 | @Override 61 | public void onStart(ITestContext context) { 62 | } 63 | 64 | @Override 65 | public void onFinish(ITestContext context) { 66 | if (!isFailed && !isSkipped) { 67 | generateCodeCoverage(bundle); 68 | } 69 | } 70 | 71 | private void generateCodeCoverage(Bundle results) { 72 | // use reflection to call emma dump coverage method, to avoid 73 | // always statically compiling against emma jar 74 | java.io.File coverageFile = new java.io.File(mCoverageFilePath); 75 | Class emmaRTClass; 76 | try { 77 | emmaRTClass = Class.forName("com.vladium.emma.rt.RT"); 78 | Method dumpCoverageMethod = emmaRTClass.getMethod("dumpCoverageData", 79 | coverageFile.getClass(), boolean.class, boolean.class); 80 | dumpCoverageMethod.invoke(null, coverageFile, false, false); 81 | 82 | // output path to generated coverage file so it can be parsed by a test harness if 83 | // needed 84 | results.putString(REPORT_KEY_COVERAGE_PATH, mCoverageFilePath); 85 | // also output a more user friendly msg 86 | Log.d(TestNGLogger.TAG, "\nGenerated code coverage data to " + mCoverageFilePath); 87 | } catch (ClassNotFoundException e) { 88 | reportEmmaError("Is emma jar on classpath?", e); 89 | } catch (SecurityException | NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 90 | reportEmmaError(e); 91 | } 92 | } 93 | 94 | private void reportEmmaError(Exception e) { 95 | reportEmmaError("", e); 96 | } 97 | 98 | private void reportEmmaError(String hint, Exception e) { 99 | String msg = "Failed to generate emma coverage. " + hint; 100 | Log.e(TestNGLogger.TAG, msg, e); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TestNG runner for Android 2 | ========================= 3 | 4 | This is a minimal implementation of an Android 5 | [Instrumentation](http://developer.android.com/reference/android/app/Instrumentation.html) 6 | executing unit tests based on [TestNG](http://testng.org/) (the best testing framework for Java). 7 | 8 | [![Download](https://api.bintray.com/packages/lemonade/maven/android-testng/images/download.svg) ](https://bintray.com/lemonade/maven/android-testng/_latestVersion) 9 | 10 | Usage 11 | ----- 12 | 13 | Depending on your build system, your mileage might vary, but with 14 | [Gradle](https://gradle.org/) the only required changes to your build files 15 | should be limited to adding our [repository](https://bintray.com/lemonade/maven), 16 | then declaring the dependency and modifying your `testInstrumentationRunner`: 17 | 18 | ```groovy 19 | // Our Bintray repository 20 | repositories { 21 | maven { 22 | url 'http://dl.bintray.com/lemonade/maven' 23 | } 24 | } 25 | 26 | // TestNG dependency, remember to update to the latest version 27 | dependencies { 28 | androidTestCompile 'de.lemona.android:android-testng:X.Y.Z' 29 | } 30 | 31 | // Android setup 32 | android { 33 | defaultConfig { 34 | testInstrumentationRunner 'de.lemona.android.testng.TestNGRunner' 35 | } 36 | } 37 | ``` 38 | 39 | 40 | Packages 41 | -------- 42 | 43 | The runner will *ONLY* look for classes in the package specified by the 44 | `targetPackage` entry in your `AndroidManifest.xml` file. 45 | 46 | In [Gradle](https://gradle.org/) this defaults to your application package 47 | plus `....test`. 48 | 49 | If no tests can be found, verify the parameter in the manifest of your APK. 50 | 51 | For example in our [manifest](src/main/AndroidManifest.xml) the declared 52 | package is `de.lemona.android.testng`, henceforth after the build processes 53 | it, all our tests will be automatically searched for in the 54 | [`de.lemona.android.testng.test`](https://github.com/LemonadeLabInc/android-testng/tree/master/src/androidTest/java/de/lemona/android/testng/test) 55 | package. 56 | 57 | 58 | XML Suites 59 | ---------- 60 | 61 | Test suites can also be defined using a [`testng.xml`](http://testng.org/doc/documentation-main.html#testng-xml) 62 | file from your [`assets`](src/androidTest/assets) directory. 63 | 64 | This is useful when tests do not reside in the standard application package 65 | plus `....test`. 66 | 67 | One caveat, though, is that the `` element does not work _(yet)_, 68 | as TestNG expects JAR files, while Android bundles everything into a DAX file. 69 | 70 | For an example see the [`testng.xml`](src/androidTest/assets/testng.xml) file 71 | included alongside these sources. 72 | 73 | 74 | Contexts 75 | -------- 76 | 77 | In order to have access to the Android's application 78 | [Context](http://developer.android.com/reference/android/content/Context.html) 79 | please refer to the [`AndroidTestNGSupport`](src/main/java/de/lemona/android/testng/AndroidTestNGSupport.java) 80 | utility class. The two static `getContext()` and `getInstrumentation()` methods 81 | allow retrieval of the instances. 82 | 83 | [Google Guice](https://github.com/google/guice) injection is also supported. Take a look at the 84 | [GuiceInjectionTest](src/androidTest/java/de/lemona/android/testng/test/GuiceInjectionTest.java) 85 | for an example of how to configure your tests. 86 | 87 | 88 | Options 89 | -------- 90 | 91 | The options to enable some features on testing are same as [adb instrument](https://developer.android.com/studio/test/command-line.html). Current supported options are as below: 92 | - debug 93 | - coverage 94 | - coverageFile 95 | 96 | If you need to run tests from Android Studio, please use [Android Tests](https://www.jetbrains.com/help/idea/2016.1/run-debug-configuration-android-test.html) Configuration. 97 | 98 | License 99 | ------- 100 | 101 | Licensed under the [Apache License version 2](LICENSE.md) 102 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/TestNGLogger.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import org.testng.IConfigurationListener; 4 | import org.testng.IResultMap; 5 | import org.testng.ISuite; 6 | import org.testng.ITestContext; 7 | import org.testng.ITestListener; 8 | import org.testng.ITestNGMethod; 9 | import org.testng.ITestResult; 10 | 11 | import android.util.Log; 12 | 13 | /** 14 | * A TestNG {@link ITestListener} logging TestNG events to Android's 15 | * {@link Log} with TestNG tag. 16 | * 17 | * You can view what's being logged using the Android SDK's 18 | * logcat 19 | * tool. 20 | */ 21 | public class TestNGLogger implements ITestListener, IConfigurationListener { 22 | 23 | public static final String TAG = "TestNG"; 24 | 25 | @Override 26 | public void onStart(ITestContext context) { 27 | final ISuite suite = context.getSuite(); 28 | final String suiteName = suite == null ? "[UNKNOWN]" : suite.getName(); 29 | 30 | final ITestNGMethod[] methods = context.getAllTestMethods(); 31 | if (methods == null) { 32 | Log.w(TAG, "No test methods provided by " + suiteName + " (null methods)"); 33 | } else if (methods.length < 1) { 34 | Log.w(TAG, "No test methods provided by " + suiteName + " (length=" + methods.length + ")"); 35 | } else { 36 | Log.i(TAG, "Starting test run \"" + suiteName + "\" with " + methods.length + " tests"); 37 | } 38 | } 39 | 40 | @Override 41 | public void onFinish(ITestContext context) { 42 | final ISuite suite = context.getSuite(); 43 | final String suiteName = suite == null ? "[UNKNOWN]" : suite.getName(); 44 | 45 | final IResultMap passed = context.getPassedTests(); 46 | final IResultMap failed = context.getFailedTests(); 47 | final IResultMap skipped = context.getSkippedTests(); 48 | 49 | final int passedCount = passed == null ? -1 : passed.size(); 50 | final int failedCount = failed == null ? -1 : failed.size(); 51 | final int skippedCount = skipped == null ? -1 : skipped.size(); 52 | 53 | Log.i(TAG, "Finished test run \"" + suiteName + "\" with " 54 | + passedCount + " successful tests, " 55 | + failedCount + " failures and " 56 | + skippedCount + " tests skipped"); 57 | } 58 | 59 | /* ====================================================================== */ 60 | 61 | @Override 62 | public void onTestStart(ITestResult result) { 63 | final String name = result.getInstanceName() + "." + result.getName(); 64 | Log.d(TAG, "Test \"" + name + "\" starting"); 65 | } 66 | 67 | @Override 68 | public void onTestSuccess(ITestResult result) { 69 | final Throwable throwable = result.getThrowable(); 70 | final long ms = result.getEndMillis() - result.getStartMillis(); 71 | final String name = result.getInstanceName() + "." + result.getName(); 72 | Log.d(TAG, "Test \"" + name + "\" successful (" + ms + " ms)", throwable); 73 | } 74 | 75 | @Override 76 | public void onTestFailure(ITestResult result) { 77 | final Throwable throwable = result.getThrowable(); 78 | final long ms = result.getEndMillis() - result.getStartMillis(); 79 | final String name = result.getInstanceName() + "." + result.getName(); 80 | Log.w(TAG, "Test \"" + name + "\" failed (" + ms + " ms)", throwable); 81 | } 82 | 83 | @Override 84 | public void onTestSkipped(ITestResult result) { 85 | final Throwable throwable = result.getThrowable(); 86 | final long ms = result.getEndMillis() - result.getStartMillis(); 87 | final String name = result.getInstanceName() + "." + result.getName(); 88 | Log.i(TAG, "Test \"" + name + "\" skipped (" + ms + " ms)", throwable); 89 | } 90 | 91 | @Override 92 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) { 93 | this.onTestFailure(result); 94 | } 95 | 96 | /* ====================================================================== */ 97 | 98 | @Override 99 | public void onConfigurationSuccess(ITestResult result) { 100 | final String name = result.getInstanceName() + "." + result.getName(); 101 | Log.i(TAG, "Configuration success: " + name); 102 | } 103 | 104 | @Override 105 | public void onConfigurationFailure(ITestResult result) { 106 | final String name = result.getInstanceName() + "." + result.getName(); 107 | Log.i(TAG, "Configuration failure: " + name); 108 | } 109 | 110 | @Override 111 | public void onConfigurationSkip(ITestResult result) { 112 | final String name = result.getInstanceName() + "." + result.getName(); 113 | Log.i(TAG, "Configuration skipped: " + name); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/TestNGRunner.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.Bundle; 5 | import android.os.Debug; 6 | import android.util.Log; 7 | 8 | import org.testng.TestNG; 9 | import org.testng.collections.Lists; 10 | import org.testng.xml.Parser; 11 | import org.testng.xml.XmlClass; 12 | import org.testng.xml.XmlSuite; 13 | import org.testng.xml.XmlTest; 14 | 15 | import java.io.FileNotFoundException; 16 | import java.io.InputStream; 17 | import java.util.Enumeration; 18 | import java.util.List; 19 | 20 | import dalvik.system.DexFile; 21 | 22 | import static de.lemona.android.testng.TestNGLogger.TAG; 23 | 24 | /** 25 | * The root of all evil, creating a TestNG {@link XmlSuite} and running it. 26 | */ 27 | public class TestNGRunner extends Instrumentation { 28 | 29 | private String targetPackage = null; 30 | private TestNGArgs args; 31 | 32 | @Override 33 | public void onCreate(Bundle arguments) { 34 | super.onCreate(arguments); 35 | args = parseRunnerArgument(arguments); 36 | targetPackage = this.getTargetContext().getPackageName(); 37 | this.start(); 38 | } 39 | 40 | private TestNGArgs parseRunnerArgument(Bundle arguments) { 41 | Log.d(TAG, "DEBUG arguments"); 42 | for (String key : arguments.keySet()) { 43 | Log.d(TAG, "key " + key + " = " + arguments.get(key)); 44 | } 45 | Log.d(TAG, "DEBUG argumetns END"); 46 | TestNGArgs.Builder builder = new TestNGArgs.Builder(this).fromBundle(arguments); 47 | return builder.build(); 48 | } 49 | 50 | @Override 51 | public void onStart() { 52 | final TestNGListener listener = new TestNGListener(this); 53 | AndroidTestNGSupport.injectInstrumentation(this); 54 | 55 | if (args.debug) { 56 | // waitForDebugger 57 | Log.d(TAG, "waiting for debugger..."); 58 | Debug.waitForDebugger(); 59 | Log.d(TAG, "debugger was connected."); 60 | } 61 | 62 | setupDexmakerClassloader(); 63 | 64 | final TestNG ng = new TestNG(false); 65 | ng.setDefaultSuiteName("Android TestNG Suite"); 66 | ng.setDefaultTestName("Android TestNG Test"); 67 | 68 | // Try to load "testng.xml" from the assets directory... 69 | try { 70 | final InputStream input = this.getContext().getAssets().open("testng.xml"); 71 | if (input != null) ng.setXmlSuites(new Parser(input).parseToList()); 72 | } catch (final FileNotFoundException exception) { 73 | Log.d(TAG, "The \"testng.xml\" file was not found in assets"); 74 | } catch (final Throwable throwable) { 75 | Log.e(TAG, "An unexpected error occurred parsing \"testng.xml\"", throwable); 76 | listener.fail(this.getClass().getName(), "onStart", throwable); 77 | } 78 | 79 | try { 80 | // Our XML suite for running tests 81 | final XmlSuite xmlSuite = new XmlSuite(); 82 | 83 | xmlSuite.setVerbose(0); 84 | xmlSuite.setJUnit(false); 85 | xmlSuite.setName(targetPackage); 86 | 87 | // Open up the DEX file associated with our APK 88 | final String apk = this.getContext().getPackageCodePath(); 89 | final DexFile dex = new DexFile(apk); 90 | final Enumeration e = dex.entries(); 91 | 92 | // Prepare our XML test and list of classes 93 | final XmlTest xmlTest = new XmlTest(xmlSuite); 94 | final List xmlClasses = Lists.newArrayList(); 95 | 96 | // Process every element of the DEX file 97 | while (e.hasMoreElements()) { 98 | final String cls = e.nextElement(); 99 | if (! cls.startsWith(targetPackage)) continue; 100 | Log.d(TAG, "Adding potential test class " + cls); 101 | 102 | try { 103 | xmlClasses.add(new XmlClass(cls, true)); 104 | } catch (final Throwable throwable) { 105 | // Likely NoClassDefException for missing dependencies 106 | Log.w(TAG, "Ignoring class " + cls, throwable); 107 | } 108 | } 109 | 110 | // Remember our classes if we have to 111 | if (! xmlClasses.isEmpty()) { 112 | Log.i(TAG, "Adding suite from package \"" + targetPackage + "\""); 113 | xmlTest.setXmlClasses(xmlClasses); 114 | ng.setCommandLineSuite(xmlSuite); 115 | } 116 | 117 | } catch (final Throwable throwable) { 118 | Log.e(TAG, "An unexpected error occurred analysing package \"" + targetPackage + "\"", throwable); 119 | listener.fail(this.getClass().getName(), "onStart", throwable); 120 | } 121 | 122 | // Run tests! 123 | try { 124 | ng.addListener(new TestNGLogger()); 125 | if (args.codeCoverage) { 126 | ng.addListener(new TestNGCoverageListener(this, args.codeCoveragePath)); 127 | } 128 | ng.addListener((Object) listener); 129 | ng.runSuitesLocally(); 130 | 131 | } catch (final Throwable throwable) { 132 | Log.e(TAG, "An unexpected error occurred running tests", throwable); 133 | listener.fail(this.getClass().getName(), "onStart", throwable); 134 | 135 | } finally { 136 | // Close our listener 137 | listener.close(); 138 | } 139 | } 140 | 141 | private void setupDexmakerClassloader() { 142 | ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); 143 | // must set the context classloader for apps that use a shared uid, see 144 | // frameworks/base/core/java/android/app/LoadedApk.java 145 | ClassLoader newClassLoader = this.getClass().getClassLoader(); 146 | //Log.i(LOG_TAG, String.format("Setting context classloader to '%s', Original: '%s'", 147 | // newClassLoader.toString(), originalClassLoader.toString())); 148 | Thread.currentThread().setContextClassLoader(newClassLoader); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/de/lemona/android/testng/TestNGListener.java: -------------------------------------------------------------------------------- 1 | package de.lemona.android.testng; 2 | 3 | import android.app.Activity; 4 | import android.app.Instrumentation; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | 8 | import org.testng.IConfigurationListener; 9 | import org.testng.IExecutionListener; 10 | import org.testng.ITestContext; 11 | import org.testng.ITestListener; 12 | import org.testng.ITestNGMethod; 13 | import org.testng.ITestResult; 14 | 15 | import java.io.Closeable; 16 | import java.io.PrintWriter; 17 | import java.io.StringWriter; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.concurrent.atomic.AtomicBoolean; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | import static android.app.Instrumentation.REPORT_KEY_IDENTIFIER; 23 | 24 | /** 25 | * A TestNG {@link ITestListener} sending status reports. 26 | */ 27 | public class TestNGListener implements ITestListener, 28 | IExecutionListener, 29 | IConfigurationListener, 30 | Closeable { 31 | 32 | /** Value for {@link Instrumentation#REPORT_KEY_IDENTIFIER} */ 33 | private static final String REPORT_VALUE_ID = "TestNGRunner"; 34 | 35 | /** Total number of tests being run (sent with all status messages). */ 36 | private static final String REPORT_KEY_NUM_TOTAL = "numtests"; 37 | /** Sequence number of the current test. */ 38 | private static final String REPORT_KEY_NUM_CURRENT = "current"; 39 | /** Name of the current test class. */ 40 | private static final String REPORT_KEY_NAME_CLASS = "class"; 41 | /** The name of the current test. */ 42 | private static final String REPORT_KEY_NAME_TEST = "test"; 43 | /** Stack trace describing an error or failure. */ 44 | private static final String REPORT_KEY_STACK = "stack"; 45 | 46 | /** Test is starting. */ 47 | private static final int REPORT_VALUE_RESULT_START = 1; 48 | /** Test completed successfully. */ 49 | private static final int REPORT_VALUE_RESULT_OK = 0; 50 | /** Test completed with an error.*/ 51 | //private static final int REPORT_VALUE_RESULT_ERROR = -1; 52 | /** Test completed with a failure. */ 53 | private static final int REPORT_VALUE_RESULT_FAILURE = -2; 54 | /** Test was skipped. */ 55 | private static final int REPORT_VALUE_RESULT_SKIPPED = -3; 56 | /** Test completed with an assumption failure. */ 57 | //private static final int REPORT_VALUE_RESULT_ASSUMPTION_FAILURE = -4; 58 | 59 | /* ====================================================================== */ 60 | 61 | private final AtomicBoolean isClosed = new AtomicBoolean(); 62 | private final AtomicInteger testNumber = new AtomicInteger(); 63 | private final ConcurrentHashMap tests; 64 | private final Instrumentation instrumentation; 65 | private final Bundle bundle; 66 | private boolean started; 67 | 68 | /** 69 | * Create a new {@link TestNGListener} instance. 70 | * 71 | * @param instrumentation The {@link Instrumentation} running tests. 72 | */ 73 | protected TestNGListener(Instrumentation instrumentation) { 74 | if (instrumentation == null) throw new NullPointerException(); 75 | this.tests = new ConcurrentHashMap<>(); 76 | this.instrumentation = instrumentation; 77 | this.bundle = new Bundle(); 78 | this.started = false; 79 | 80 | // Always set... 81 | bundle.putString(REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); 82 | } 83 | 84 | /* ====================================================================== */ 85 | 86 | private final void sendStatus(int status) { 87 | if (isClosed.compareAndSet(false, false)) { 88 | Log.d("TestNG", "Sending " + status + ": " + bundle); 89 | this.instrumentation.sendStatus(status, bundle); 90 | } 91 | } 92 | 93 | private final void sendStatus(int status, ITestResult result) { 94 | sendStatus(status, result.getThrowable()); 95 | } 96 | 97 | private final void sendStatus(int status, Throwable throwable) { 98 | if (throwable != null) { 99 | final StringWriter writer = new StringWriter(); 100 | final PrintWriter printer = new PrintWriter(writer); 101 | throwable.printStackTrace(printer); 102 | printer.flush(); 103 | writer.flush(); 104 | final String trace = writer.toString() 105 | .replaceAll("(?m)^[ \t]*\r?\n", "") 106 | .trim(); 107 | bundle.putString(REPORT_KEY_STACK, trace); 108 | } else { 109 | bundle.remove(REPORT_KEY_STACK); 110 | } 111 | 112 | sendStatus(status); 113 | } 114 | 115 | /* ====================================================================== */ 116 | 117 | /** 118 | * Close ths instance, notifying the {@link Instrumentation} that all 119 | * tests have been executed. 120 | * 121 | * This method will call {@link Instrumentation#finish(int, Bundle)} only 122 | * once and once called will prevent further notifications. 123 | */ 124 | @Override 125 | public void close() { 126 | if (isClosed.compareAndSet(false, true)) { 127 | bundle.remove(REPORT_KEY_NUM_CURRENT); 128 | bundle.remove(REPORT_KEY_NAME_CLASS); 129 | bundle.remove(REPORT_KEY_NAME_TEST); 130 | bundle.remove(REPORT_KEY_STACK); 131 | instrumentation.finish(Activity.RESULT_OK, bundle); 132 | } 133 | } 134 | 135 | /** Report an unexpected failure in the tests */ 136 | void fail(String className, String testName, Throwable throwable) { 137 | if (! started) bundle.putInt(REPORT_KEY_NUM_TOTAL, 1); 138 | 139 | bundle.putInt(REPORT_KEY_NUM_CURRENT, testNumber.incrementAndGet()); 140 | bundle.putString(REPORT_KEY_NAME_CLASS, className); 141 | bundle.putString(REPORT_KEY_NAME_TEST, testName); 142 | 143 | sendStatus(REPORT_VALUE_RESULT_START); 144 | sendStatus(REPORT_VALUE_RESULT_FAILURE, throwable); 145 | } 146 | 147 | /* ====================================================================== */ 148 | 149 | /** 150 | * Notify that we are about to start testing. 151 | * 152 | * This method will setup the initial {@link Bundle} for notifications. 153 | */ 154 | @Override 155 | public void onStart(ITestContext context) { 156 | this.started = true; 157 | 158 | final ITestNGMethod[] methods = context.getAllTestMethods(); 159 | 160 | if ((methods == null) || (methods.length < 1)) { 161 | bundle.putInt(REPORT_KEY_NUM_TOTAL, 0); 162 | } else { 163 | bundle.putInt(REPORT_KEY_NUM_TOTAL, methods.length); 164 | } 165 | } 166 | 167 | @Override 168 | public void onFinish(ITestContext context) { 169 | // Let this be closed by the instrumentation runner for when 170 | // there are multiple suites (e.g. from "testng.xml" in assets) 171 | } 172 | 173 | /* ====================================================================== */ 174 | 175 | @Override 176 | public void onTestStart(ITestResult result) { 177 | final String className = result.getInstanceName(); 178 | final String resultName = result.getName(); 179 | final String name = className + '.' + resultName; 180 | 181 | // Test methods can be invoked mutiple times, with data providers! 182 | if (! tests.contains(name)) tests.putIfAbsent(name, new AtomicInteger(0)); 183 | final AtomicInteger count = tests.get(name); 184 | 185 | final int num = count.getAndIncrement(); 186 | final String testName = num == 0 ? resultName : resultName + '#' + num; 187 | 188 | bundle.putInt(REPORT_KEY_NUM_CURRENT, testNumber.incrementAndGet()); 189 | bundle.putString(REPORT_KEY_NAME_CLASS, className); 190 | bundle.putString(REPORT_KEY_NAME_TEST, testName); 191 | 192 | sendStatus(REPORT_VALUE_RESULT_START); 193 | } 194 | 195 | @Override 196 | public void onTestSuccess(ITestResult result) { 197 | sendStatus(REPORT_VALUE_RESULT_OK, result); 198 | } 199 | 200 | @Override 201 | public void onTestSkipped(ITestResult result) { 202 | sendStatus(REPORT_VALUE_RESULT_SKIPPED, result); 203 | } 204 | 205 | @Override 206 | public void onTestFailure(ITestResult result) { 207 | sendStatus(REPORT_VALUE_RESULT_FAILURE, result); 208 | } 209 | 210 | @Override 211 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) { 212 | sendStatus(REPORT_VALUE_RESULT_FAILURE, result); 213 | } 214 | 215 | /* ====================================================================== */ 216 | 217 | @Override 218 | public void onExecutionStart() { 219 | // Nothing to do really... 220 | } 221 | 222 | @Override 223 | public void onExecutionFinish() { 224 | this.close(); 225 | } 226 | 227 | /* ====================================================================== */ 228 | 229 | @Override 230 | public void onConfigurationSuccess(ITestResult result) { 231 | // We don't report any configuration success... 232 | } 233 | 234 | @Override 235 | public void onConfigurationFailure(ITestResult result) { 236 | // Emulate test failure 237 | this.onTestStart(result); 238 | this.onTestFailure(result); 239 | } 240 | 241 | @Override 242 | public void onConfigurationSkip(ITestResult result) { 243 | // Emulate test skipped 244 | this.onTestStart(result); 245 | this.onTestSkipped(result); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | [Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0.html) 5 | 6 | 1 - Definitions 7 | --------------- 8 | 9 | * *"License"* shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | * *"Licensor"* shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | * *"Legal Entity"* shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of fifty 20 | percent (50%) or more of the outstanding shares, or (iii) beneficial 21 | ownership of such entity. 22 | 23 | * *"You"* (or *"Your"*) shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | * *"Source"* form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, 28 | and configuration files. 29 | 30 | * *"Object"* form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to other media types. 33 | 34 | * *"Work"* shall mean the work of authorship, whether in Source or Object form, 35 | made available under the License, as indicated by a copyright notice that 36 | is included in or attached to the work (an example is provided in the 37 | Appendix below). 38 | 39 | * *"Derivative Works"* shall mean any work, whether in Source or Object form, 40 | that is based on (or derived from) the Work and for which the editorial 41 | revisions, annotations, elaborations, or other modifications represent, as 42 | a whole, an original work of authorship. For the purposes of this License, 43 | Derivative Works shall not include works that remain separable from, or 44 | merely link (or bind by name) to the interfaces of, the Work and Derivative 45 | Works thereof. 46 | 47 | * *"Contribution"* shall mean any work of authorship, including the original 48 | version of the Work and any modifications or additions to that Work or 49 | Derivative Works thereof, that is intentionally submitted to Licensor for 50 | inclusion in the Work by the copyright owner or by an individual or Legal 51 | Entity authorized to submit on behalf of the copyright owner. For the 52 | purposes of this definition, "submitted" means any form of electronic, 53 | verbal, or written communication sent to the Licensor or its 54 | representatives, including but not limited to communication on electronic 55 | mailing lists, source code control systems, and issue tracking systems that 56 | are managed by, or on behalf of, the Licensor for the purpose of discussing 57 | and improving the Work, but excluding communication that is conspicuously 58 | marked or otherwise designated in writing by the copyright owner as "Not a 59 | Contribution." 60 | 61 | * *"Contributor"* shall mean Licensor and any individual or Legal Entity on 62 | behalf of whom a Contribution has been received by Licensor and 63 | subsequently incorporated within the Work. 64 | 65 | 66 | 2 - Grant of Copyright License 67 | ------------------------------ 68 | 69 | Subject to the terms and conditions of this License, each Contributor 70 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable copyright license to reproduce, prepare 72 | Derivative Works of, publicly display, publicly perform, sublicense, and 73 | distribute the Work and such Derivative Works in Source or Object form. 74 | 75 | 76 | 3 - Grant of Patent License 77 | --------------------------- 78 | 79 | Subject to the terms and conditions of this License, each Contributor 80 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 81 | royalty-free, irrevocable (except as stated in this section) patent license 82 | to make, have made, use, offer to sell, sell, import, and otherwise 83 | transfer the Work, where such license applies only to those patent claims 84 | licensable by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) with the 86 | Work to which such Contribution(s) was submitted. If You institute patent 87 | litigation against any entity (including a cross-claim or counterclaim in a 88 | lawsuit) alleging that the Work or a Contribution incorporated within the 89 | Work constitutes direct or contributory patent infringement, then any 90 | patent licenses granted to You under this License for that Work shall 91 | terminate as of the date such litigation is filed. 92 | 93 | 94 | 4 - Redistribution 95 | ------------------ 96 | 97 | You may reproduce and distribute copies of the Work or Derivative Works 98 | thereof in any medium, with or without modifications, and in Source or 99 | Object form, provided that You meet the following conditions: 100 | 101 | 1. You must give any other recipients of the Work or Derivative Works a 102 | copy of this License; and 103 | 104 | 2. You must cause any modified files to carry prominent notices stating 105 | that You changed the files; and 106 | 107 | 3. You must retain, in the Source form of any Derivative Works that You 108 | distribute, all copyright, patent, trademark, and attribution notices 109 | from the Source form of the Work, excluding those notices that do not 110 | pertain to any part of the Derivative Works; and 111 | 112 | 4. If the Work includes a "NOTICE" text file as part of its distribution, 113 | then any Derivative Works that You distribute must include a readable 114 | copy of the attribution notices contained within such NOTICE file, 115 | excluding those notices that do not pertain to any part of the 116 | Derivative Works, in at least one of the following places: within a 117 | NOTICE text file distributed as part of the Derivative Works; within the 118 | Source form or documentation, if provided along with the Derivative 119 | Works; or, within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents of the 121 | NOTICE file are for informational purposes only and do not modify the 122 | License. You may add Your own attribution notices within Derivative 123 | Works that You distribute, alongside or as an addendum to the NOTICE 124 | text from the Work, provided that such additional attribution notices 125 | cannot be construed as modifying the License. You may add Your own 126 | copyright statement to Your modifications and may provide additional or 127 | different license terms and conditions for use, reproduction, or 128 | distribution of Your modifications, or for any such Derivative Works as 129 | a whole, provided Your use, reproduction, and distribution of the Work 130 | otherwise complies with the conditions stated in this License. 131 | 132 | 133 | 5 - Submission of Contributions 134 | ------------------------------- 135 | 136 | Unless You explicitly state otherwise, any Contribution intentionally 137 | submitted for inclusion in the Work by You to the Licensor shall be under 138 | the terms and conditions of this License, without any additional terms or 139 | conditions. Notwithstanding the above, nothing herein shall supersede or 140 | modify the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 144 | 6 - Trademarks 145 | -------------- 146 | 147 | This License does not grant permission to use the trade names, trademarks, 148 | service marks, or product names of the Licensor, except as required for 149 | reasonable and customary use in describing the origin of the Work and 150 | reproducing the content of the NOTICE file. 151 | 152 | 153 | 7 - Disclaimer of Warranty 154 | -------------------------- 155 | 156 | Unless required by applicable law or agreed to in writing, Licensor 157 | provides the Work (and each Contributor provides its Contributions) on an 158 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 159 | or implied, including, without limitation, any warranties or conditions of 160 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR 161 | PURPOSE. You are solely responsible for determining the appropriateness of 162 | using or redistributing the Work and assume any risks associated with Your 163 | exercise of permissions under this License. 164 | 165 | 166 | 8 - Limitation of Liability 167 | --------------------------- 168 | 169 | In no event and under no legal theory, whether in tort (including 170 | negligence), contract, or otherwise, unless required by applicable law 171 | (such as deliberate and grossly negligent acts) or agreed to in writing, 172 | shall any Contributor be liable to You for damages, including any direct, 173 | indirect, special, incidental, or consequential damages of any character 174 | arising as a result of this License or out of the use or inability to use 175 | the Work (including but not limited to damages for loss of goodwill, work 176 | stoppage, computer failure or malfunction, or any and all other commercial 177 | damages or losses), even if such Contributor has been advised of the 178 | possibility of such damages. 179 | 180 | 9 - Accepting Warranty or Additional Liability 181 | ---------------------------------------------- 182 | 183 | While redistributing the Work or Derivative Works thereof, You may choose 184 | to offer, and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this License. 186 | However, in accepting such obligations, You may act only on Your own behalf 187 | and on Your sole responsibility, not on behalf of any other Contributor, 188 | and only if You agree to indemnify, defend, and hold each Contributor 189 | harmless for any liability incurred by, or claims asserted against, such 190 | Contributor by reason of your accepting any such warranty or additional 191 | liability. 192 | --------------------------------------------------------------------------------