├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mogujie │ │ └── natasha │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mogujie │ │ │ └── natasha │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── mogujie │ └── natasha │ ├── ExampleUnitTest.java │ ├── MainActivityTest.java │ └── TestTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mogujie │ │ └── lib │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mogujie │ │ │ └── natasha │ │ │ ├── ActivityRule.java │ │ │ ├── ActivityTestBase.java │ │ │ ├── BuildConfigClass.java │ │ │ ├── JSpec.java │ │ │ ├── JSpecRule.java │ │ │ ├── MockResponse.java │ │ │ ├── MockServer.java │ │ │ ├── ReflectionHelpers.java │ │ │ ├── RobolectricTestBase.java │ │ │ ├── TestBase.java │ │ │ └── ViewTestBase.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── mogujie │ └── lib │ └── UserModelTest.java ├── settings.gradle └── unit_testing.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # android generated 5 | bin/ 6 | gen/ 7 | 8 | # eclipse 9 | .classpath 10 | .settings 11 | .project 12 | 13 | local.properties 14 | project.properties 15 | 16 | # IDEA 17 | .idea/ 18 | *.iml 19 | out/ 20 | 21 | # gradle 22 | build/ 23 | .gradle/ 24 | gradlew.bat 25 | 26 | # build 27 | lint.xml 28 | lint.html 29 | proguard-rules.txt 30 | 31 | #local gradle configurations 32 | local.gradle 33 | 34 | .gradletasknamecache 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 蘑菇街 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 这个project是怎么来的? 3 | 4 | 蘑菇街目前采用组件化的开发方式,一个app由很多个模块组成,每个模块都有单元测试的部分,然而有很多代码都是类似的。因此,为了减少重复劳动,我们花时间抽出来一个独立的project,专门做unit testing用的。 5 | 6 | ## 这个project是干什么用的? 7 | 8 | 如前所述,这个project里面主要是一些unit testing会用到的公共代码,来帮助你更快的做unit testing,减少一些boilerplate code。 9 | 说白了就是一些帮助类,里面有些帮助方法。 10 | 比如里面有个TestBase,里面有 11 | 12 | ``` 13 | // <=> 是等效于的意思 14 | ae() => Assert.assertEquals() 15 | at() => Assert.assertTrue() 16 | af() => Assert.assertFalse() 17 | ``` 18 | 19 | 目前还没有完整的文档,要看都有哪些帮助类,哪些方法可以看源码哦,目前总共也没几个。 20 | 总之,有了这个project,单元测试将变得更加的简单。 21 | 22 | ## 这个project用到那些框架/技术? 23 | JUnit4, Robolectric,Mockito,AssertJ,Gson 24 | 如果你不懂什么叫unit test,请看[这里](http://chriszou.com/2016/04/13/android-unit-testing-start-from-what.html) 25 | 如果你不懂JUnit的使用,请看[这里](http://chriszou.com/2016/04/18/android-unit-testing-junit.html) 26 | 如果你不懂Mockito的使用,请看[这里](http://chriszou.com/2016/04/29/android-unit-testing-mockito.html) 27 | 如果你不懂Robolectric的使用,请看[这里](http://chriszou.com/2016/06/05/robolectric-android-on-jvm.html) 28 | 有任何单元测试的问题,请看[这里](http://chriszou.com/) 29 | 30 | 31 | ## 怎么样使用? 32 | 目前你可以使用http://jitpack.io/来引入这个项目 33 | 34 | ```groovy 35 | allprojects { 36 | repositories { 37 | maven { url "https://jitpack.io" } 38 | } 39 | } 40 | 41 | dependencies { 42 | compile 'com.github.mogujie:natasha:v0.1.1' 43 | } 44 | ``` 45 | ## 一些小例子 46 | 47 | ```java 48 | public class StringUtils { 49 | public static boolean isEmpty(String str) { 50 | return str == null || str.length() == 0; 51 | } 52 | } 53 | 54 | //测试纯Java代码,继承TestBase 55 | public class StringUtilsTest extends TestBase { 56 | 57 | @Test 58 | public void should_isEmpty_works() { 59 | at(StringUtils.isEmpty(null)); //at() is short for Assert.assertTrue() 60 | at(StringUtils.isEmpty("")); 61 | af(StringUtils.isEmpty(" ")); //af() is short for Assert.assertFalse() 62 | af(StringUtils.isEmpty("hello")); 63 | } 64 | 65 | } 66 | 67 | //测试Android相关的代码,继承RobolectricTestBase,同时需要指定constants = BuildConfig.class,不然的话,会报资源找不到的错误 68 | @Config( constants = BuildConfig.class ) 69 | public class ColorUtilsTest extends RobolectricTestBase { 70 | @Test 71 | public void should_parseColor_valid_color() { 72 | ae(Color.RED, Color.parseColor("#FF0000")); 73 | ae(Color.WHITE, Color.parseColor("#FFFFFF")); 74 | } 75 | } 76 | ``` 77 | 78 | 欢迎各种PR,建议以及吐槽! -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | android { 7 | useLibrary 'org.apache.http.legacy' 8 | compileSdkVersion 23 9 | buildToolsVersion "23.0.2" 10 | 11 | defaultConfig { 12 | applicationId "com.mogujie.natasha" 13 | minSdkVersion 15 14 | targetSdkVersion 23 15 | versionCode 1 16 | versionName "1.0" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | compile 'junit:junit:4.12' 29 | compile 'com.android.support:appcompat-v7:23.0.0' 30 | testCompile project(':lib') 31 | } 32 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/xiaochuang/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mogujie/natasha/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/mogujie/natasha/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | public class MainActivity extends AppCompatActivity { 7 | 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | setContentView(R.layout.activity_main); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | natasha 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/mogujie/natasha/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mogujie/natasha/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.robolectric.RobolectricGradleTestRunner; 6 | import org.robolectric.annotation.Config; 7 | 8 | /** 9 | * Created by xiaochuang on 4/22/16. 10 | */ 11 | @RunWith(RobolectricGradleTestRunner.class) 12 | @Config(constants = BuildConfig.class, sdk = 21) 13 | public class MainActivityTest extends ActivityTestBase { 14 | 15 | @Test 16 | @JSpec(desc = "should activity not null") 17 | public void testactivitynotnull() { 18 | ann(getActivity()); 19 | } 20 | 21 | @Override 22 | protected Class activityClass() { 23 | return MainActivity.class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/test/java/com/mogujie/natasha/TestTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | /** 4 | * Created by xiaochuang on 4/22/16. 5 | */ 6 | public class TestTest extends TestBase { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.1' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meili/natasha/cf545d7f0c8425c4c79152c22371e7e6561b3018/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 01 17:59:00 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | useLibrary 'org.apache.http.legacy' 5 | compileSdkVersion 23 6 | buildToolsVersion "23.0.2" 7 | 8 | defaultConfig { 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | repositories { 23 | maven { url "https://jitpack.io" } 24 | jcenter() 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | compile 'com.android.support:support-annotations:23.0.0' 30 | 31 | compile 'junit:junit:4.12' 32 | compile 'org.robolectric:robolectric:3.0' 33 | compile 'org.robolectric:shadows-support-v4:3.0' 34 | compile "org.mockito:mockito-core:1.10.19" 35 | compile 'com.google.code.gson:gson:2.4' 36 | compile 'org.assertj:assertj-core:2.3.0' 37 | } 38 | -------------------------------------------------------------------------------- /lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/xiaochuang/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /lib/src/androidTest/java/com/mogujie/lib/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.lib; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/ActivityRule.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | 6 | import org.junit.rules.TestRule; 7 | import org.junit.runner.Description; 8 | import org.junit.runners.model.Statement; 9 | import org.robolectric.Robolectric; 10 | 11 | /** 12 | * Created by xiaochuang on 4/22/16. 13 | */ 14 | public class ActivityRule implements TestRule { 15 | private final Intent intent; 16 | private final Class clazz; 17 | private final boolean needStartActivity; 18 | private T mActivity; 19 | 20 | public ActivityRule(Class clazz, Intent intent, boolean needStartActivity) { 21 | this.intent = intent; 22 | this.clazz = clazz; 23 | this.needStartActivity = needStartActivity; 24 | } 25 | 26 | public T getActivity() { 27 | return mActivity; 28 | } 29 | 30 | @Override 31 | public Statement apply(Statement base, Description description) { 32 | return new ActivityStatement(base); 33 | } 34 | 35 | private T setupActivity() { 36 | return Robolectric.buildActivity(clazz).withIntent(intent).create().get(); 37 | } 38 | 39 | private void finishActivity() { 40 | if (mActivity != null) mActivity.finish(); 41 | } 42 | 43 | private class ActivityStatement extends Statement { 44 | private final Statement mBase; 45 | 46 | public ActivityStatement(Statement base) { 47 | mBase = base; 48 | } 49 | 50 | @Override 51 | public void evaluate() throws Throwable { 52 | try { 53 | if (needStartActivity) mActivity = setupActivity(); 54 | mBase.evaluate(); 55 | } finally { 56 | finishActivity(); 57 | } 58 | } 59 | } 60 | 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/ActivityTestBase.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.support.annotation.NonNull; 6 | import android.view.View; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.robolectric.Robolectric; 13 | import org.robolectric.Shadows; 14 | import org.robolectric.shadows.ShadowToast; 15 | import org.robolectric.util.ReflectionHelpers; 16 | 17 | /** 18 | * Created by xiaochuang on 1/7/16. 19 | */ 20 | public abstract class ActivityTestBase extends ViewTestBase { 21 | 22 | protected T mActivity; 23 | 24 | @Override 25 | protected View createView() { 26 | return Shadows.shadowOf(getActivity()).getContentView(); 27 | } 28 | 29 | /** 30 | * Will start the activity under testing 31 | */ 32 | @Before 33 | public void setup() { 34 | mActivity = setupActivity(activityClass(), activityIntent()); 35 | super.setup(); 36 | } 37 | 38 | @Test 39 | public void testDumb() { 40 | at(true); 41 | } 42 | 43 | /** 44 | * Intent to build this activity, used in Robolectric.setupActivity() 45 | * Override this method if you want to build the target activity with a specific intent 46 | * @return 47 | */ 48 | public Intent activityIntent() { 49 | return null; 50 | } 51 | 52 | /** 53 | * Get the activity under testing 54 | * @return the activity under testing 55 | */ 56 | public T getActivity() { 57 | return mActivity; 58 | // return activityRule.getActivity(); 59 | } 60 | 61 | public T setupActivity(Class clazz, Intent intent) { 62 | return Robolectric.buildActivity(clazz).withIntent(intent).create().get(); 63 | } 64 | 65 | public T setupActivity() { 66 | return setupActivity(activityClass(), activityIntent()); 67 | } 68 | 69 | /** 70 | * Override this method and return the class of the target activity (the activity under testing) 71 | * 72 | * @return the class of the target activity 73 | */ 74 | protected abstract Class activityClass(); 75 | 76 | /** 77 | * Assert that a toast was shown 78 | * 79 | * @param text the text of the toast 80 | */ 81 | protected void assertToast(String text) { 82 | ae(text, ShadowToast.getTextOfLatestToast()); 83 | } 84 | 85 | /** 86 | * Assert the view with the given id was enabled 87 | * 88 | * @param viewId the view id that should be enabled 89 | */ 90 | protected void assertEnabled(int viewId) { 91 | at(view(viewId).isEnabled()); 92 | } 93 | 94 | /** 95 | * Return the view with the given id, same as calling findViewById(viewId) on the target activity 96 | * 97 | * @param viewId the id of the view to return 98 | * @return a View with the given Id 99 | */ 100 | protected View view(int viewId) { 101 | return getActivity().findViewById(viewId); 102 | } 103 | 104 | /** 105 | * Assert that the view with the given id has the given text 106 | * 107 | * @param viewId 108 | * @param text 109 | */ 110 | protected void assertViewHasText(int viewId, String text) { 111 | String viewText = textOfView(viewId); 112 | at("Expect: " + viewText + " to contain: " + text, viewText.contains(text)); 113 | } 114 | 115 | 116 | /** 117 | * Get the text of the view with the given id 118 | * 119 | * @param viewId 120 | * @return 121 | */ 122 | @NonNull 123 | private String textOfView(int viewId) { 124 | return tv(viewId).getText().toString(); 125 | } 126 | 127 | /** 128 | * Return the TextView with the given id, same as calling (TextView)findViewById(viewId) on the target activity 129 | * 130 | * @param viewId 131 | * @return A TextView with the given id 132 | */ 133 | private TextView tv(int viewId) { 134 | return (TextView) view(viewId); 135 | } 136 | 137 | public void assertActivityHasField(String fieldName) { 138 | ann(ReflectionHelpers.getField(getActivity(), fieldName)); 139 | } 140 | 141 | protected void assertViewVisible(View view) { 142 | ae(View.VISIBLE, view.getVisibility()); 143 | } 144 | 145 | protected void assertViewVisible(int viewId) { 146 | assertViewVisible(view(viewId)); 147 | } 148 | 149 | protected void assertViewHasText(int viewId, int textRes) { 150 | assertViewHasText(viewId, getString(textRes)); 151 | } 152 | 153 | private String getString(int textRes) { 154 | return getActivity().getString(textRes); 155 | } 156 | 157 | protected void assertViewText(int viewId, String text) { 158 | ae(text, textOfView(viewId)); 159 | } 160 | 161 | protected void assertViewText(int viewId, int textRes) { 162 | assertViewText(viewId, getString(textRes)); 163 | } 164 | 165 | 166 | protected void assertViewExists(int viewId) { 167 | ann(view(viewId)); 168 | } 169 | 170 | protected void click(int viewId) { 171 | view(viewId).performClick(); 172 | } 173 | 174 | protected void assertViewGone(View view) { 175 | ae(View.GONE, view.getVisibility()); 176 | } 177 | 178 | public void assertViewGone(int viewId) { 179 | assertViewGone(view(viewId)); 180 | } 181 | 182 | protected LinearLayout ll(int llId) { 183 | return (LinearLayout) view(llId); 184 | } 185 | 186 | protected void assertNextActivity(Class clazz) { 187 | Intent intent = Shadows.shadowOf(getActivity()).getNextStartedActivity(); 188 | ae(new Intent(getActivity(), clazz), intent); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/BuildConfigClass.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | /** 4 | * Created by xiaochuang on 4/22/16. 5 | */ 6 | public class BuildConfigClass { 7 | private static Class sBuildConfigClass; 8 | 9 | public static Class get() { 10 | return sBuildConfigClass; 11 | } 12 | 13 | public static void setBuildConfigClass(Class clazz) { 14 | sBuildConfigClass = clazz; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/JSpec.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Created by xiaochuang on 4/6/16. 10 | */ 11 | @Target(ElementType.METHOD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface JSpec { 14 | /** 15 | * A description of the test case. Let you get rid of wring "_" to test case names 16 | * @return 17 | */ 18 | String desc() default ""; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/JSpecRule.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | 7 | /** 8 | * Created by xiaochuang on 4/22/16. 9 | */ 10 | public class JSpecRule implements TestRule { 11 | @Override 12 | public Statement apply(final Statement base, final Description description) { 13 | final JSpec annotation = description.getAnnotation(JSpec.class); 14 | if (annotation == null) return base; 15 | 16 | return new Statement() { 17 | @Override 18 | public void evaluate() throws Throwable { 19 | String msg = description.getTestClass().getSimpleName() + "#" + description.getMethodName() + ": " + annotation.desc() + "..."; 20 | try { 21 | base.evaluate(); 22 | } catch (Throwable e) { 23 | String detailMessage = msg + "Failed!"; 24 | if (e.getMessage() != null) detailMessage += e.getMessage(); 25 | Throwable throwable = new Throwable(detailMessage, e);//, e); 26 | throwable.setStackTrace(e.getStackTrace()); 27 | throw throwable; 28 | } 29 | System.out.print(msg); 30 | System.out.println("passed"); 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/MockResponse.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import com.google.gson.Gson; 4 | 5 | /** 6 | * Created by xiaochuang on 1/8/16. 7 | */ 8 | public class MockResponse { 9 | public final int code; 10 | public final String msg; 11 | public final String json; 12 | 13 | public MockResponse(String json) { 14 | this(200, null, json); 15 | } 16 | 17 | public MockResponse(Object data) { 18 | this(200, null, toJson(data)); 19 | } 20 | 21 | private static String toJson(Object data) { 22 | return new Gson().toJson(data); 23 | } 24 | 25 | 26 | public MockResponse(String msg, int code) { 27 | this(code, msg, null); 28 | } 29 | 30 | public MockResponse(int code, String msg, String json) { 31 | this.code = code; 32 | this.msg = msg; 33 | this.json = json; 34 | } 35 | 36 | public boolean successful() { 37 | return json != null; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "Status: "+ code+", msg: "+msg+", body: "+json; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/MockServer.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * Created by xiaochuang on 1/8/16. 10 | */ 11 | public class MockServer { 12 | 13 | private static final Map sRoutes = new HashMap<>(); 14 | 15 | private static void addRoute(String url, String data) { 16 | sRoutes.put(url, new MockResponse(data)); 17 | } 18 | 19 | private static void addRoute(String url, Object data) { 20 | sRoutes.put(url, new MockResponse(data)); 21 | } 22 | 23 | /** 24 | * Put your test code in testRunnable. 25 | * When running test, the request for {@code url} will get a {@link MockResponse} with the json field to be the json format of {@code data} 26 | * 27 | * @param url the target url 28 | * @param json json to return as in the returned {@link MockResponse} if a request for {@code url} is requested 29 | * @param testRunnable the test code to run. 30 | */ 31 | public static void withRoute(String url, String json, Runnable testRunnable) { 32 | addRoute(url, json); 33 | testRunnable.run(); 34 | removeRoute(url); 35 | } 36 | 37 | public static void withRoute(String url, Object data, Runnable runnable) { 38 | addRoute(url, data); 39 | runnable.run(); 40 | removeRoute(url); 41 | } 42 | 43 | @NonNull 44 | public static MockResponse get(String url) { 45 | log("Got request: "+url); 46 | MockResponse mockResponse = sRoutes.get(url); 47 | if (mockResponse==null) mockResponse = new MockResponse("Not found", 404); 48 | log("Returning: "+mockResponse); 49 | return mockResponse; 50 | } 51 | 52 | public static void removeRoute(String s) { 53 | sRoutes.remove(s); 54 | } 55 | 56 | private static void log(String str) { 57 | System.out.println(str); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/ReflectionHelpers.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Modifier; 8 | 9 | /** 10 | * Collection of helper methods for calling methods and accessing fields reflectively. 11 | */ 12 | public class ReflectionHelpers { 13 | 14 | /** 15 | * Reflectively get the value of a field. 16 | * 17 | * @param object Target object. 18 | * @param fieldName The field name. 19 | * @param The return type. 20 | * @return Value of the field on the object. 21 | */ 22 | @SuppressWarnings("unchecked") 23 | public static R getField(final Object object, final String fieldName) { 24 | try { 25 | return traverseClassHierarchy(object.getClass(), NoSuchFieldException.class, new InsideTraversal() { 26 | @Override 27 | public R run(Class traversalClass) throws Exception { 28 | Field field = traversalClass.getDeclaredField(fieldName); 29 | field.setAccessible(true); 30 | return (R) field.get(object); 31 | } 32 | }); 33 | } catch (Exception e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | 38 | /** 39 | * Reflectively set the value of a field. 40 | * 41 | * @param object Target object. 42 | * @param fieldName The field name. 43 | * @param fieldNewValue New value. 44 | */ 45 | public static void setField(final Object object, final String fieldName, final Object fieldNewValue) { 46 | try { 47 | traverseClassHierarchy(object.getClass(), NoSuchFieldException.class, new InsideTraversal() { 48 | @Override 49 | public Void run(Class traversalClass) throws Exception { 50 | Field field = traversalClass.getDeclaredField(fieldName); 51 | field.setAccessible(true); 52 | field.set(object, fieldNewValue); 53 | return null; 54 | } 55 | }); 56 | } catch (Exception e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | /** 62 | * Reflectively set the value of a field. 63 | * 64 | * @param type Target type. 65 | * @param object Target object. 66 | * @param fieldName The field name. 67 | * @param fieldNewValue New value. 68 | */ 69 | public static void setField(Class type, final Object object, final String fieldName, final Object fieldNewValue) { 70 | try { 71 | Field field = type.getDeclaredField(fieldName); 72 | field.setAccessible(true); 73 | field.set(object, fieldNewValue); 74 | } catch (Exception e) { 75 | throw new RuntimeException(e); 76 | } 77 | } 78 | 79 | /** 80 | * Reflectively get the value of a static field. 81 | * 82 | * @param field Field object. 83 | * @param The return type. 84 | * @return Value of the field. 85 | */ 86 | @SuppressWarnings("unchecked") 87 | public static R getStaticField(Field field) { 88 | try { 89 | makeFieldVeryAccessible(field); 90 | return (R) field.get(null); 91 | } catch (Exception e) { 92 | throw new RuntimeException(e); 93 | } 94 | } 95 | 96 | /** 97 | * Reflectively get the value of a static field. 98 | * 99 | * @param clazz Target class. 100 | * @param fieldName The field name. 101 | * @param The return type. 102 | * @return Value of the field. 103 | */ 104 | public static R getStaticField(Class clazz, String fieldName) { 105 | try { 106 | return getStaticField(clazz.getDeclaredField(fieldName)); 107 | } catch (Exception e) { 108 | throw new RuntimeException(e); 109 | } 110 | } 111 | 112 | /** 113 | * Reflectively set the value of a static field. 114 | * 115 | * @param field Field object. 116 | * @param fieldNewValue The new value. 117 | */ 118 | public static void setStaticField(Field field, Object fieldNewValue) { 119 | try { 120 | makeFieldVeryAccessible(field); 121 | field.set(null, fieldNewValue); 122 | } catch (Exception e) { 123 | throw new RuntimeException(e); 124 | } 125 | } 126 | 127 | /** 128 | * Reflectively set the value of a static field. 129 | * 130 | * @param clazz Target class. 131 | * @param fieldName The field name. 132 | * @param fieldNewValue The new value. 133 | */ 134 | public static void setStaticField(Class clazz, String fieldName, Object fieldNewValue) { 135 | try { 136 | setStaticField(clazz.getDeclaredField(fieldName), fieldNewValue); 137 | } catch (Exception e) { 138 | throw new RuntimeException(e); 139 | } 140 | } 141 | 142 | /** 143 | * Reflectively call an instance method on an object. 144 | * 145 | * @param instance Target object. 146 | * @param methodName The method name to call. 147 | * @param classParameters Array of parameter types and values. 148 | * @param The return type. 149 | * @return The return value of the method. 150 | */ 151 | public static R callInstanceMethod(final Object instance, final String methodName, ClassParameter... classParameters) { 152 | try { 153 | final Class[] classes = ClassParameter.getClasses(classParameters); 154 | final Object[] values = ClassParameter.getValues(classParameters); 155 | 156 | return traverseClassHierarchy(instance.getClass(), NoSuchMethodException.class, new InsideTraversal() { 157 | @Override 158 | @SuppressWarnings("unchecked") 159 | public R run(Class traversalClass) throws Exception { 160 | Method declaredMethod = traversalClass.getDeclaredMethod(methodName, classes); 161 | declaredMethod.setAccessible(true); 162 | return (R) declaredMethod.invoke(instance, values); 163 | } 164 | }); 165 | } catch (InvocationTargetException e) { 166 | if (e.getTargetException() instanceof RuntimeException) { 167 | throw (RuntimeException) e.getTargetException(); 168 | } 169 | if (e.getTargetException() instanceof Error) { 170 | throw (Error) e.getTargetException(); 171 | } 172 | throw new RuntimeException(e.getTargetException()); 173 | } catch (Exception e) { 174 | throw new RuntimeException(e); 175 | } 176 | } 177 | 178 | /** 179 | * Reflectively call an instance method on an object on a specific class. 180 | * 181 | * @param cl The class. 182 | * @param instance Target object. 183 | * @param methodName The method name to call. 184 | * @param classParameters Array of parameter types and values. 185 | * @param The return type. 186 | * @return The return value of the method. 187 | */ 188 | public static R callInstanceMethod(Class cl, final Object instance, final String methodName, ClassParameter... classParameters) { 189 | try { 190 | final Class[] classes = ClassParameter.getClasses(classParameters); 191 | final Object[] values = ClassParameter.getValues(classParameters); 192 | 193 | Method declaredMethod = cl.getDeclaredMethod(methodName, classes); 194 | declaredMethod.setAccessible(true); 195 | return (R) declaredMethod.invoke(instance, values); 196 | } catch (InvocationTargetException e) { 197 | if (e.getTargetException() instanceof RuntimeException) { 198 | throw (RuntimeException) e.getTargetException(); 199 | } 200 | if (e.getTargetException() instanceof Error) { 201 | throw (Error) e.getTargetException(); 202 | } 203 | throw new RuntimeException(e.getTargetException()); 204 | } catch (Exception e) { 205 | throw new RuntimeException(e); 206 | } 207 | } 208 | 209 | /** 210 | * Reflectively call a static method on a class. 211 | * 212 | * @param clazz Target class. 213 | * @param methodName The method name to call. 214 | * @param classParameters Array of parameter types and values. 215 | * @param The return type. 216 | * @return The return value of the method. 217 | */ 218 | @SuppressWarnings("unchecked") 219 | public static R callStaticMethod(Class clazz, String methodName, ClassParameter... classParameters) { 220 | try { 221 | Class[] classes = ClassParameter.getClasses(classParameters); 222 | Object[] values = ClassParameter.getValues(classParameters); 223 | 224 | Method method = clazz.getDeclaredMethod(methodName, classes); 225 | method.setAccessible(true); 226 | return (R) method.invoke(null, values); 227 | } catch (InvocationTargetException e) { 228 | if (e.getTargetException() instanceof RuntimeException) { 229 | throw (RuntimeException) e.getTargetException(); 230 | } 231 | if (e.getTargetException() instanceof Error) { 232 | throw (Error) e.getTargetException(); 233 | } 234 | throw new RuntimeException(e.getTargetException()); 235 | } catch (Exception e) { 236 | throw new RuntimeException(e); 237 | } 238 | } 239 | 240 | /** 241 | * Load a class. 242 | * 243 | * @param classLoader The class loader. 244 | * @param fullyQualifiedClassName The fully qualified class name. 245 | * @return The class object. 246 | */ 247 | public static Class loadClass(ClassLoader classLoader, String fullyQualifiedClassName) { 248 | try { 249 | return classLoader.loadClass(fullyQualifiedClassName); 250 | } catch (ClassNotFoundException e) { 251 | throw new RuntimeException(e); 252 | } 253 | } 254 | 255 | /** 256 | * Create a new instance of a class 257 | * 258 | * @param cl The class object. 259 | * @param The class type. 260 | * @return New class instance. 261 | */ 262 | public static T newInstance(Class cl) { 263 | try { 264 | return cl.newInstance(); 265 | } catch (InstantiationException | IllegalAccessException e) { 266 | throw new RuntimeException(e); 267 | } 268 | } 269 | 270 | /** 271 | * Reflectively call the constructor of an object. 272 | * 273 | * @param clazz Target class. 274 | * @param classParameters Array of parameter types and values. 275 | * @param The return type. 276 | * @return The return value of the method. 277 | */ 278 | public static R callConstructor(Class clazz, ClassParameter... classParameters) { 279 | try { 280 | final Class[] classes = ClassParameter.getClasses(classParameters); 281 | final Object[] values = ClassParameter.getValues(classParameters); 282 | 283 | Constructor constructor = clazz.getDeclaredConstructor(classes); 284 | constructor.setAccessible(true); 285 | return constructor.newInstance(values); 286 | } catch (InstantiationException e) { 287 | throw new RuntimeException("error instantiating " + clazz.getName(), e); 288 | } catch (InvocationTargetException e) { 289 | if (e.getTargetException() instanceof RuntimeException) { 290 | throw (RuntimeException) e.getTargetException(); 291 | } 292 | if (e.getTargetException() instanceof Error) { 293 | throw (Error) e.getTargetException(); 294 | } 295 | throw new RuntimeException(e.getTargetException()); 296 | } catch (Exception e) { 297 | throw new RuntimeException(e); 298 | } 299 | } 300 | 301 | private static R traverseClassHierarchy(Class targetClass, Class exceptionClass, InsideTraversal insideTraversal) throws Exception { 302 | Class hierarchyTraversalClass = targetClass; 303 | while (true) { 304 | try { 305 | return insideTraversal.run(hierarchyTraversalClass); 306 | } catch (Exception e) { 307 | if (!exceptionClass.isInstance(e)) { 308 | throw e; 309 | } 310 | hierarchyTraversalClass = hierarchyTraversalClass.getSuperclass(); 311 | if (hierarchyTraversalClass == null) { 312 | throw new RuntimeException(e); 313 | } 314 | } 315 | } 316 | } 317 | 318 | private static void makeFieldVeryAccessible(Field field) throws NoSuchFieldException, IllegalAccessException { 319 | field.setAccessible(true); 320 | 321 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 322 | modifiersField.setAccessible(true); 323 | modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); 324 | } 325 | 326 | private interface InsideTraversal { 327 | R run(Class traversalClass) throws Exception; 328 | } 329 | 330 | /** 331 | * Typed parameter used with reflective method calls. 332 | * 333 | * @param The value of the method parameter. 334 | */ 335 | public static class ClassParameter { 336 | public final Class clazz; 337 | public final V val; 338 | 339 | public ClassParameter(Class clazz, V val) { 340 | this.clazz = clazz; 341 | this.val = val; 342 | } 343 | 344 | public static ClassParameter from(Class clazz, V val) { 345 | return new ClassParameter<>(clazz, val); 346 | } 347 | 348 | public static ClassParameter[] fromComponentLists(Class[] classes, Object[] values) { 349 | ClassParameter[] classParameters = new ClassParameter[classes.length]; 350 | for (int i = 0; i < classes.length; i++) { 351 | classParameters[i] = ClassParameter.from(classes[i], values[i]); 352 | } 353 | return classParameters; 354 | } 355 | 356 | public static Class[] getClasses(ClassParameter... classParameters) { 357 | Class[] classes = new Class[classParameters.length]; 358 | for (int i = 0; i < classParameters.length; i++) { 359 | Class paramClass = classParameters[i].clazz; 360 | classes[i] = paramClass; 361 | } 362 | return classes; 363 | } 364 | 365 | public static Object[] getValues(ClassParameter... classParameters) { 366 | Object[] values = new Object[classParameters.length]; 367 | for (int i = 0; i < classParameters.length; i++) { 368 | Object paramValue = classParameters[i].val; 369 | values[i] = paramValue; 370 | } 371 | return values; 372 | } 373 | } 374 | 375 | /** 376 | * String parameter used with reflective method calls. 377 | * 378 | * @param The value of the method parameter. 379 | */ 380 | public static class StringParameter { 381 | public final String className; 382 | public final V val; 383 | 384 | public StringParameter(String className, V val) { 385 | this.className = className; 386 | this.val = val; 387 | } 388 | 389 | public static StringParameter from(String className, V val) { 390 | return new StringParameter<>(className, val); 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/RobolectricTestBase.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.app.Application; 4 | 5 | import com.mogujie.lib.BuildConfig; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.robolectric.RobolectricGradleTestRunner; 10 | import org.robolectric.RuntimeEnvironment; 11 | import org.robolectric.annotation.Config; 12 | import org.robolectric.shadows.ShadowLog; 13 | 14 | /** 15 | * Created by xiaochuang on 1/7/16. 16 | */ 17 | @RunWith(RobolectricGradleTestRunner.class) 18 | @Config(constants = BuildConfig.class, sdk = 21) 19 | public class RobolectricTestBase extends TestBase { 20 | 21 | public RobolectricTestBase() { 22 | ShadowLog.stream = System.out; 23 | } 24 | 25 | /** 26 | * The context in testing environment 27 | * @return 28 | */ 29 | public Application getContext() { 30 | return RuntimeEnvironment.application; 31 | } 32 | 33 | /** 34 | * Just to get rid of the "No test method found" error 35 | */ 36 | @Test 37 | public void testDumb() { 38 | at(true); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/TestBase.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | /** 4 | * Created by xiaochuang on 3/4/16. 5 | */ 6 | 7 | import com.google.gson.Gson; 8 | 9 | import junit.framework.Assert; 10 | 11 | import org.junit.Rule; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.MockitoJUnit; 14 | import org.mockito.junit.MockitoRule; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static junit.framework.Assert.assertEquals; 22 | import static org.junit.Assert.assertNotNull; 23 | 24 | /** 25 | * Extend Mockito so that any method in Mockito is make available in TestBase, OH YEAH, I'm a genius!!! 26 | * Why? Because very time you mock or spy, you have to static import the method or use Mockito.methodYouWant, which is annoying! 27 | * Created by xiaochuang on 12/17/15. 28 | */ 29 | public class TestBase extends Mockito { 30 | 31 | /** 32 | * Inject mocks before every test method runs 33 | */ 34 | @Rule 35 | public MockitoRule mockitoRule = MockitoJUnit.rule(); 36 | 37 | @Rule 38 | public JSpecRule jSpecRule = new JSpecRule(); 39 | 40 | public CountDownLatch latch = new CountDownLatch(1); 41 | 42 | /** 43 | * Short for assertEquals 44 | * @param str1 45 | * @param str2 46 | */ 47 | public void ae(String str1, String str2) { 48 | assertEquals(str1, str2); 49 | } 50 | 51 | /** 52 | * Short for Assert.assertNotNull 53 | * @param single 54 | */ 55 | public void ann(Object single) { 56 | assertNotNull(single); 57 | } 58 | 59 | /** 60 | * Short for Assert.assertEqual 61 | * @param expected 62 | * @param actual 63 | */ 64 | protected void ae(int expected, int actual) { 65 | assertEquals(expected, actual); 66 | } 67 | 68 | /** 69 | * Short for Assert.assertEqual 70 | * 71 | * @param msg 72 | * @param expected 73 | * @param actual 74 | */ 75 | protected void ae(String msg, int expected, int actual) { 76 | assertEquals(msg, expected, actual); 77 | } 78 | 79 | /** 80 | * Short for Assert.assertTrue 81 | * @param condition 82 | */ 83 | protected void at(boolean condition) { 84 | Assert.assertTrue(condition); 85 | } 86 | 87 | 88 | /** 89 | * Short for Assert.assertTrue 90 | * 91 | * @param condition 92 | */ 93 | protected void at(String msg, boolean condition) { 94 | Assert.assertTrue(msg, condition); 95 | } 96 | 97 | 98 | /** 99 | * Short for Assert.assertTrue(true); 100 | */ 101 | protected void at() { 102 | at(true); 103 | } 104 | 105 | 106 | /** 107 | * Short for Assert.assertNull 108 | * @param object 109 | */ 110 | protected void an(Object object) { 111 | org.junit.Assert.assertNull(object); 112 | } 113 | 114 | /** 115 | * Same as Assert.fail, save you from annoying static import. 116 | */ 117 | protected void fail() { 118 | Assert.fail(); 119 | } 120 | 121 | /** 122 | * Short for Assert.assertFalse 123 | * @param b 124 | */ 125 | protected void af(String msg, boolean b) { 126 | Assert.assertFalse(msg, b); 127 | } 128 | 129 | /** 130 | * Short for Assert.assertFalse 131 | * 132 | * @param b 133 | */ 134 | protected void af(boolean b) { 135 | Assert.assertFalse(b); 136 | } 137 | 138 | /** 139 | * Short for Assert.assertEqual 140 | * @param expected 141 | * @param actual 142 | */ 143 | protected void ae(float expected, float actual) { 144 | assertEquals(expected, actual); 145 | } 146 | 147 | /** 148 | * Short for Assert.assertEqual 149 | * @param expected 150 | * @param actual 151 | */ 152 | protected void ae(Object expected, Object actual) { 153 | assertEquals(expected, actual); 154 | } 155 | 156 | /** 157 | * Assert these two are of the same class 158 | * @param expected 159 | * @param actual 160 | */ 161 | protected void ac(Object expected, Object actual) { 162 | ae(expected.getClass(), actual.getClass()); 163 | } 164 | 165 | /** 166 | * Put your test data file under src/test/resources/file_name.json, and pass in 'file_name.json', this method will 167 | * read the content of the file as String and use Gson to convert to an Object of the class in the argument. 168 | * @param resourceName the file name of your test resource 169 | * @param clazz the class of Object you want to return 170 | * @param 171 | * @return an Object of T 172 | * @throws IOException 173 | */ 174 | public T dataFromResource(String resourceName, Class clazz) { 175 | return json2Data(readResource(resourceName), clazz); 176 | } 177 | 178 | public String readResource(String resourceName) { 179 | try { 180 | InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName); 181 | byte[] bytes = new byte[is.available()]; 182 | is.read(bytes); 183 | return new String(bytes, "UTF-8"); 184 | } catch (IOException e) { 185 | throw new RuntimeException(e); 186 | } 187 | } 188 | 189 | public void resetLatch(int count) { 190 | latch = new CountDownLatch(count); 191 | } 192 | 193 | public void await() throws InterruptedException { 194 | latch.await(); 195 | } 196 | 197 | public boolean await(long timeout, TimeUnit unit) throws InterruptedException { 198 | return latch.await(timeout, unit); 199 | } 200 | 201 | public void countDown() { 202 | latch.countDown(); 203 | } 204 | 205 | /** 206 | * 207 | * @param json 208 | * @param clazz 209 | * @param 210 | * @return 211 | */ 212 | private T json2Data(String json, Class clazz) { 213 | return new Gson().fromJson(json, clazz); 214 | } 215 | 216 | } 217 | 218 | -------------------------------------------------------------------------------- /lib/src/main/java/com/mogujie/natasha/ViewTestBase.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.natasha; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import org.junit.Before; 7 | 8 | /** 9 | * Base class for testing views, provide many helper methods for velidation views 10 | * Created by xiaochuang on 3/3/16. 11 | * @param 12 | */ 13 | public abstract class ViewTestBase extends RobolectricTestBase { 14 | 15 | protected T mTargetView; 16 | @Before 17 | public void setup() { 18 | mTargetView = createView(); 19 | } 20 | 21 | /** 22 | * Create the target view under testing 23 | * 24 | * @return the view under testing 25 | */ 26 | protected abstract T createView(); 27 | 28 | /** 29 | * Get a child view of the target view 30 | * 31 | * @param id the id of the child view 32 | * @return 33 | */ 34 | public View child(int id) { 35 | return mTargetView.findViewById(id); 36 | } 37 | 38 | /** 39 | * Check that given view has visibility of {@code View.VISIBLE} 40 | * 41 | * @param view the view to be checked 42 | */ 43 | protected void assertViewVisible(View view) { 44 | ae(View.VISIBLE, view.getVisibility()); 45 | } 46 | 47 | /** 48 | * Check that the given view has visibility of {@code View.GONE} 49 | * 50 | * @param view the child view to be checked 51 | */ 52 | protected void assertViewGone(View view) { 53 | ae("Expect view to be gone, but was: " + getReadableVisibility(view.getVisibility()), View.GONE, view.getVisibility()); 54 | } 55 | 56 | private String getReadableVisibility(int visibility) { 57 | switch (visibility) { 58 | case View.GONE: 59 | return "GONE"; 60 | case View.VISIBLE: 61 | return "VISIBLE"; 62 | case View.INVISIBLE: 63 | return "INVISIBLE"; 64 | default: 65 | return "UNKNOWN"; 66 | } 67 | } 68 | 69 | /** 70 | * Check that the child view with the given view id has visibility of {@code View.GONE} 71 | * 72 | * @param childViewId the id of the child view to be checked 73 | */ 74 | public void assertChildGone(int childViewId) { 75 | assertViewGone(child(childViewId)); 76 | } 77 | 78 | /** 79 | * Check that the child view with the given view id has visibility of {@code View.VISIBLE} 80 | * 81 | * @param childViewId the id of the child view to be checked 82 | */ 83 | public void assertChildVisible(int childViewId) { 84 | assertViewVisible(child(childViewId)); 85 | } 86 | 87 | private ViewGroup vg(int viewGroupId) { 88 | return (ViewGroup) child(viewGroupId); 89 | } 90 | 91 | public int getVisibleChildCount(int viewGroupId) { 92 | ViewGroup vg = vg(viewGroupId); 93 | return getVisibleChildCount(vg); 94 | } 95 | 96 | public int getVisibleChildCount() { 97 | return getVisibleChildCount(mTargetView); 98 | } 99 | 100 | protected int getVisibleChildCount(View vg) { 101 | int count = 0; 102 | for (int i = 0; i < ((ViewGroup) vg).getChildCount(); i++) { 103 | View v = ((ViewGroup) vg).getChildAt(i); 104 | if (v.getVisibility() == View.VISIBLE) count++; 105 | } 106 | 107 | return count; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | lib 3 | 4 | -------------------------------------------------------------------------------- /lib/src/test/java/com/mogujie/lib/UserModelTest.java: -------------------------------------------------------------------------------- 1 | package com.mogujie.lib; 2 | 3 | import com.mogujie.natasha.JSpec; 4 | import com.mogujie.natasha.TestBase; 5 | 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | 9 | /** 10 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 11 | */ 12 | public class UserModelTest extends TestBase { 13 | 14 | @Test 15 | @JSpec(desc = "should display this description") 16 | public void testJSpecRule2() { 17 | at(true); 18 | } 19 | 20 | @Mock Object string; 21 | 22 | @Test 23 | @JSpec(desc = "should string mock not null") 24 | public void testmockrule() { 25 | ann(string); 26 | } 27 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':lib' 2 | -------------------------------------------------------------------------------- /unit_testing.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | jcenter() 3 | } 4 | 5 | //这个是用来代码覆盖率统计的 6 | apply plugin: 'jacoco' 7 | 8 | jacoco { 9 | toolVersion = "0.7.1.201405082137" 10 | } 11 | 12 | task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { 13 | group = "Reporting" 14 | description = "Generate Jacoco coverage reports" 15 | 16 | //1. 这里是一些不需要统计的类,比如说自动生成的一些类,可以根据需要,在这里添加 17 | def excludedSources = ['**/R.class', 18 | '**/R$*.class', 19 | '**/BuildConfig.*', 20 | '**/*$ViewInjector*.*', 21 | '**/*MembersInjector.*', 22 | '**/dagger/*', 23 | '**/Manifest*.*'] 24 | 25 | classDirectories = fileTree( 26 | dir: 'build/intermediates/classes/debug', 27 | excludes: excludedSources 28 | ) 29 | 30 | sourceDirectories = files(['src/main/java']) 31 | 32 | executionData = files('build/jacoco/testDebugUnitTest.exec') 33 | 34 | reports { 35 | xml.enabled = true 36 | html.enabled = true 37 | } 38 | 39 | } 40 | --------------------------------------------------------------------------------