Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ecd0d7c0 | ||
|
|
93db467f43 | ||
|
|
a78931ae81 | ||
|
|
93ad1fa94f | ||
|
|
2780ccc6fa | ||
|
|
fd3b088f59 | ||
|
|
e812aa6126 | ||
|
|
b949f57d54 | ||
|
|
0551240c7d | ||
|
|
8a3a0cf6bf | ||
|
|
d3ab246c78 | ||
|
|
142b0e5a1e | ||
|
|
38221a67ef | ||
|
|
993c178adf | ||
|
|
3786c6251e | ||
|
|
e5f8e5244c | ||
|
|
e13dce7ac6 | ||
|
|
3265803dec | ||
|
|
ef3891bd57 | ||
|
|
fcbdb7275b | ||
|
|
ea4c6c5b3e | ||
|
|
381ff52515 | ||
|
|
c5f39dfae3 | ||
|
|
7f8ef70e57 | ||
|
|
23af64475a | ||
|
|
7181b49211 | ||
|
|
cd7b9cc922 | ||
|
|
46288fa0e9 | ||
|
|
e61597ba8b | ||
|
|
c577301493 | ||
|
|
a33908db41 | ||
|
|
4ea97fe13f | ||
|
|
7cda8ffd36 | ||
|
|
1da98a0364 | ||
|
|
c6d892da2d | ||
|
|
d1f2c3da97 | ||
|
|
f40cfe929a | ||
|
|
96d1f9d633 | ||
|
|
350802fc9b | ||
|
|
0ae5163435 | ||
|
|
0e581a7eeb | ||
|
|
b68ff99800 | ||
|
|
7754f828cc | ||
|
|
3d62c91291 | ||
|
|
14c3d841c5 | ||
|
|
5f6cf3888c | ||
|
|
75c6a67b8f | ||
|
|
d11402dd6c | ||
|
|
db771b9c66 | ||
|
|
338c7828c8 | ||
|
|
8ebe2a5d0c | ||
|
|
4f3e3f0093 | ||
|
|
9924d7d2fc | ||
|
|
9f5867ed76 | ||
|
|
be0a580a04 | ||
|
|
78d7ee21a6 | ||
|
|
af409a65a8 | ||
|
|
a67619d574 | ||
|
|
b89daa22c3 | ||
|
|
e948e625ea | ||
|
|
d463f863db | ||
|
|
31140bef87 | ||
|
|
d2944a88fb | ||
|
|
9d6e4dd1fa | ||
|
|
9c09bf9653 | ||
|
|
6d87653fa2 | ||
|
|
eec88f4bd9 | ||
|
|
5921ccab07 | ||
|
|
8dde402a8c | ||
|
|
d10478f95f | ||
|
|
0c063e9b1f | ||
|
|
b8771f0dac | ||
|
|
42dd58584b | ||
|
|
ce55bb4c9a | ||
|
|
cff2d1ae5a | ||
|
|
5904ad99cb | ||
|
|
74fbc5b53f | ||
|
|
f6b5bab250 | ||
|
|
f2feb3cc68 |
10
README.md
@@ -11,17 +11,13 @@
|
||||
|
||||
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
## 项目维护说明
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
由于当前并行项目较多,此项目暂时转入维护阶段,在此期间`TestableMock`会继续提供不定期的版本更新。
|
||||
|
||||
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.7` 完善Mock功能的防代码重构机制,支持快速Mock指定类型的所有方法
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将尽快回复并处理。
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
|
||||
|
||||
- `v0.6` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
|
||||
- `v0.7` better anti-refactor support of mocking, allow quick mock all method in specified class
|
||||
- `v0.7` better anti-refactoring support of mocking, allow quick mock all method in specified class
|
||||
- `v1.0` all functions are stable, a brand-new start
|
||||
|
||||
## Directory Structure
|
||||
|
||||
15
demo/android-demo/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
1
demo/android-demo/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
55
demo/android-demo/app/build.gradle
Normal file
@@ -0,0 +1,55 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.alibaba.testable.demo"
|
||||
minSdkVersion 30
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
all {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-rc01'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
testImplementation 'androidx.test:core:1.4.0-alpha05'
|
||||
testImplementation 'androidx.test:runner:1.4.0-alpha05'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
testImplementation 'com.alibaba.testable:testable-all:0.6.8'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
21
demo/android-demo/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.alibaba.testable.demo", appContext.packageName)
|
||||
}
|
||||
}
|
||||
13
demo/android-demo/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.alibaba.testable.demo">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TestableAndroidDemo" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
public class DemoBasic {
|
||||
|
||||
/**
|
||||
* method with new operation
|
||||
*/
|
||||
public String newFunc() {
|
||||
BlackBox component = new BlackBox("something");
|
||||
return component.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* method with member method invoke
|
||||
*/
|
||||
public String outerFunc(String s) throws Exception {
|
||||
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* method with common method invoke
|
||||
*/
|
||||
public String commonFunc() {
|
||||
return "anything".trim() + "__" + "anything".substring(1, 2) + "__" + "abc".startsWith("ab");
|
||||
}
|
||||
|
||||
/**
|
||||
* method with static method invoke
|
||||
*/
|
||||
public BlackBox getBox() {
|
||||
return BlackBox.secretBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
public String callerOne() {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
|
||||
public String callerTwo() {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
|
||||
private static String staticFunc() {
|
||||
return "_STATIC_TAIL";
|
||||
}
|
||||
|
||||
private String innerFunc(String s) {
|
||||
return "Nothing";
|
||||
}
|
||||
|
||||
private String callFromDifferentMethod() {
|
||||
return "realOne";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* 演示与Robolectric配合使用
|
||||
* Demonstrate using mock along with Robolectric
|
||||
*/
|
||||
public class DemoService extends Service {
|
||||
|
||||
static final String TAG = "DemoService";
|
||||
SharedPreferences mPreference;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if(intent != null && "start_foreground".equalsIgnoreCase(intent.getAction())) {
|
||||
Log.d(TAG, "start service.");
|
||||
} else if(intent != null && "stop_foreground".equalsIgnoreCase(intent.getAction())) {
|
||||
Log.d(TAG, "stop service.");
|
||||
}
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
public BlackBox(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static BlackBox secretBox() {
|
||||
return new BlackBox("secret");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String something) {
|
||||
data = something;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor() {
|
||||
return "black";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
protected String data;
|
||||
|
||||
abstract public void put(String something);
|
||||
|
||||
public String get() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
String getColor();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
demo/android-demo/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
16
demo/android-demo/app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.TestableAndroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
10
demo/android-demo/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
demo/android-demo/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Testable Android Demo</string>
|
||||
</resources>
|
||||
16
demo/android-demo/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.TestableAndroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
public class DemoBasicTest {
|
||||
|
||||
private DemoBasic demoBasic = new DemoBasic();
|
||||
|
||||
public static class Mock {
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne":
|
||||
return "mock_one";
|
||||
default:
|
||||
return "mock_others";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_new_object() {
|
||||
assertEquals("mock_something", demoBasic.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoBasic.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
verify("staticFunc").with();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoBasic.commonFunc());
|
||||
verify("trim").withTimes(1);
|
||||
verify("sub").withTimes(1);
|
||||
verify("startsWith").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_static_method() {
|
||||
assertEquals("not_secret_box", demoBasic.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_get_source_method_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoBasic.callerOne() + "_" + demoBasic.callerTwo());
|
||||
// asynchronous
|
||||
assertEquals("mock_one_mock_others",
|
||||
Executors.newSingleThreadExecutor().submit(() -> demoBasic.callerOne() + "_" + demoBasic.callerTwo()).get());
|
||||
verify("callFromDifferentMethod").withTimes(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoBasic.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoBasic.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 30)
|
||||
public class DemoServiceTest {
|
||||
|
||||
private DemoService demoService;
|
||||
|
||||
public static class Mock {
|
||||
@MockMethod(targetClass = Log.class, targetMethod = "d")
|
||||
public static int log(String tag, String msg) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
demoService = Robolectric.setupService(DemoService.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartCommand() {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.setAction("start_foreground");
|
||||
demoService.onStartCommand(intent, 0, 1);
|
||||
verify("log").with("DemoService", "start service.");
|
||||
|
||||
intent.setAction("stop_foreground");
|
||||
demoService.onStartCommand(intent, 0, 1);
|
||||
verify("log").with("DemoService", "stop service.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
log.file = null
|
||||
28
demo/android-demo/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.4.32"
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.0.0-alpha14"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
21
demo/android-demo/gradle.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
6
demo/android-demo/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Fri Apr 30 14:35:40 CST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
172
demo/android-demo/gradlew
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
demo/android-demo/gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
2
demo/android-demo/settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
include ':app'
|
||||
rootProject.name = "Testable Android Demo"
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.1')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.8')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.8')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.6.1</testable.version>
|
||||
<testable.version>0.6.8</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
/**
|
||||
* 目标类,此类中的一些调用将会被Mock掉
|
||||
* Target class, some invocations inside this class will be mocked
|
||||
*/
|
||||
public class CookerService {
|
||||
|
||||
private static String hireSandwichCooker() {
|
||||
return "Real-Sandwich-Cooker";
|
||||
}
|
||||
|
||||
private static String hireHamburgerCooker() {
|
||||
return "Real-Hamburger-Cooker";
|
||||
}
|
||||
|
||||
private String cookSandwich() {
|
||||
return "Real-Sandwich";
|
||||
}
|
||||
|
||||
private String cookHamburger() {
|
||||
return "Real-Hamburger";
|
||||
}
|
||||
|
||||
public String prepareSandwich() {
|
||||
return hireSandwichCooker() + " & " + cookSandwich();
|
||||
}
|
||||
|
||||
public String prepareHamburger() {
|
||||
return hireHamburgerCooker() + " & " + cookHamburger();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
/**
|
||||
* 被测类,会访问`CookerService`里的方法
|
||||
* Class to be tested, which will access methods in TargetService class
|
||||
*/
|
||||
public class SellerService {
|
||||
|
||||
private CookerService cookerService = new CookerService();
|
||||
|
||||
public String sellSandwich() {
|
||||
return cookerService.prepareSandwich();
|
||||
}
|
||||
|
||||
public String sellHamburger() {
|
||||
return cookerService.prepareHamburger();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
|
||||
class CookerServiceMock {
|
||||
|
||||
@MockMethod(targetClass = CookerService.class)
|
||||
public static String hireSandwichCooker() {
|
||||
return "Fake-Sandwich-Cooker";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class, scope = MockScope.ASSOCIATED)
|
||||
public static String hireHamburgerCooker() {
|
||||
return "Fake-Hamburger-Cooker";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class)
|
||||
private String cookSandwich() {
|
||||
return "Faked-Sandwich";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class, scope = MockScope.ASSOCIATED)
|
||||
private String cookHamburger() {
|
||||
return "Faked-Hamburger";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SellerServiceTest {
|
||||
|
||||
private SellerService sellerService = new SellerService();
|
||||
|
||||
@Test
|
||||
void should_sell_sandwich() {
|
||||
assertEquals("Fake-Sandwich-Cooker & Faked-Sandwich", sellerService.sellSandwich());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_sell_hamburger() {
|
||||
assertEquals("Real-Hamburger-Cooker & Real-Hamburger", sellerService.sellHamburger());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.alibaba.demo.basic;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -94,7 +93,7 @@ class DemoMockTest {
|
||||
|
||||
@Test
|
||||
void should_mock_static_method() {
|
||||
Assertions.assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.core.tool.PrivateAccessor.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@@ -20,39 +20,39 @@ class DemoPrivateAccessorTest {
|
||||
@Test
|
||||
void should_access_private_method() {
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
assertEquals("member", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc"));
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
|
||||
assertEquals("member", invoke(demoPrivateAccess, "privateFunc"));
|
||||
assertEquals("abc + hello + 1", invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(Integer.valueOf(3), get(demoPrivateAccess, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_static_method() {
|
||||
assertEquals("static", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
|
||||
assertEquals("static", invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
|
||||
assertEquals("hello + 1", invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_static_field() {
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(Integer.valueOf(3), getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_update_final_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), get(demoPrivateAccess, "pi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_use_null_parameter() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", null);
|
||||
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
set(demoPrivateAccess, "pi", null);
|
||||
assertNull(get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
2
demo/java-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
omni.constructor.enhance.enable = true
|
||||
mock.target.checking.enable = true
|
||||
@@ -9,6 +9,7 @@ version = "1.0.0-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -16,8 +17,8 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.1")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.1")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.8")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.8")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.6.1</testable.version>
|
||||
<testable.version>0.6.8</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
/**
|
||||
* 目标类,此类中的一些调用将会被Mock掉
|
||||
* Target class, some invocations inside this class will be mocked
|
||||
*/
|
||||
class CookerService {
|
||||
|
||||
private fun cookSandwich(): String {
|
||||
return "Real-Sandwich"
|
||||
}
|
||||
|
||||
private fun cookHamburger(): String {
|
||||
return "Real-Hamburger"
|
||||
}
|
||||
|
||||
fun prepareSandwich(): String {
|
||||
return hireSandwichCooker() + " & " + cookSandwich()
|
||||
}
|
||||
|
||||
fun prepareHamburger(): String {
|
||||
return hireHamburgerCooker() + " & " + cookHamburger()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun hireSandwichCooker(): String {
|
||||
return "Real-Sandwich-Cooker"
|
||||
}
|
||||
|
||||
private fun hireHamburgerCooker(): String {
|
||||
return "Real-Hamburger-Cooker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
/**
|
||||
* 被测类,会访问`CookerService`里的方法
|
||||
* Class to be tested, which will access methods in TargetService class
|
||||
*/
|
||||
class SellerService {
|
||||
|
||||
private val cookerService = CookerService()
|
||||
|
||||
fun sellSandwich(): String {
|
||||
return cookerService.prepareSandwich()
|
||||
}
|
||||
|
||||
fun sellHamburger(): String {
|
||||
return cookerService.prepareHamburger()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.model.MockScope
|
||||
|
||||
internal class CookerServiceMock {
|
||||
|
||||
@MockMethod(targetClass = CookerService::class)
|
||||
private fun cookSandwich(): String {
|
||||
return "Faked-Sandwich"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class, scope = MockScope.ASSOCIATED)
|
||||
private fun cookHamburger(): String {
|
||||
return "Faked-Hamburger"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class)
|
||||
fun hireSandwichCooker(): String {
|
||||
return "Fake-Sandwich-Cooker"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class, scope = MockScope.ASSOCIATED)
|
||||
fun hireHamburgerCooker(): String {
|
||||
return "Fake-Hamburger-Cooker"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SellerServiceTest {
|
||||
|
||||
private val sellerService = SellerService()
|
||||
|
||||
@Test
|
||||
fun should_sell_sandwich() {
|
||||
assertEquals("Fake-Sandwich-Cooker & Faked-Sandwich", sellerService.sellSandwich())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_sell_hamburger() {
|
||||
assertEquals("Real-Hamburger-Cooker & Real-Hamburger", sellerService.sellHamburger())
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.Box
|
||||
import com.alibaba.demo.basic.model.mock.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = com.alibaba.demo.basic.DemoInherit()
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
@@ -52,41 +52,41 @@ internal class DemoInheritTest {
|
||||
fun should_mock_call_sub_object_method_by_parent_object() {
|
||||
val box = demoInherit.putIntoBox() as BlackBox
|
||||
InvokeVerifier.verify("put_into_box").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_box", box.get())
|
||||
assertEquals("put_data_into_box", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_sub_object_method_by_sub_object() {
|
||||
val box = demoInherit.putIntoBlackBox()
|
||||
InvokeVerifier.verify("put_into_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_blackbox", box.get())
|
||||
assertEquals("put_data_into_blackbox", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_parent_object_method_by_parent_object() {
|
||||
val content = demoInherit.fromBox
|
||||
InvokeVerifier.verify("get_from_box").withTimes(1)
|
||||
Assertions.assertEquals("get_from_box", content)
|
||||
assertEquals("get_from_box", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_parent_object_method_by_sub_object() {
|
||||
val content = demoInherit.fromBlackBox
|
||||
InvokeVerifier.verify("get_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("get_from_blackbox", content)
|
||||
assertEquals("get_from_blackbox", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_interface_method_by_interface_object() {
|
||||
val color = demoInherit.colorViaColor
|
||||
InvokeVerifier.verify("get_color_from_color").withTimes(1)
|
||||
Assertions.assertEquals("color_from_color", color)
|
||||
assertEquals("color_from_color", color)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_interface_method_by_sub_class_object() {
|
||||
val color = demoInherit.colorViaBox
|
||||
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("color_from_blackbox", color)
|
||||
assertEquals("color_from_blackbox", color)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -21,8 +21,8 @@ internal class DemoInnerClassTest {
|
||||
@Throws(Exception::class)
|
||||
fun should_mock_invoke_inside_inner_class() {
|
||||
val demo = DemoInnerClass()
|
||||
Assertions.assertEquals("MockedCall", demo.callInnerDemo())
|
||||
Assertions.assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
Assertions.assertEquals("MockedCall", DemoInnerClass.StaticInner().demo())
|
||||
assertEquals("MockedCall", demo.callInnerDemo())
|
||||
assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
assertEquals("MockedCall", DemoInnerClass.StaticInner().demo())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = com.alibaba.demo.basic.DemoMatcher()
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: com.alibaba.demo.basic.DemoMatcher) {
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: com.alibaba.demo.basic.DemoMatcher, a1: Any, a2: Any) {
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: com.alibaba.demo.basic.DemoMatcher, a: Array<Any>) {
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import com.alibaba.demo.basic.model.omni.Child
|
||||
import com.alibaba.demo.basic.model.omni.Parent
|
||||
import com.alibaba.testable.core.tool.OmniAccessor
|
||||
import com.alibaba.testable.core.tool.OmniConstructor
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -18,14 +19,14 @@ internal class DemoOmniMethodsTest {
|
||||
val parent = OmniConstructor.newInstance(Parent::class.java)
|
||||
|
||||
// 任意深度的子孙成员对象都不为空
|
||||
Assertions.assertNotNull(parent.child?.grandChild?.content)
|
||||
assertNotNull(parent.child?.grandChild?.content)
|
||||
|
||||
// 所有基础类型初始化为默认数值
|
||||
Assertions.assertEquals(0, parent.child?.grandChild?.value)
|
||||
Assertions.assertEquals("", parent.child?.grandChild?.content)
|
||||
assertEquals(0, parent.child?.grandChild?.value)
|
||||
assertEquals("", parent.child?.grandChild?.content)
|
||||
|
||||
// 所有数组类型初始化为空数组
|
||||
Assertions.assertEquals(0, parent.children?.size)
|
||||
assertEquals(0, parent.children?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -39,23 +40,23 @@ internal class DemoOmniMethodsTest {
|
||||
|
||||
// 使用成员名称快速读取成员对象
|
||||
var contents = OmniAccessor.get<String?>(parent, "content")
|
||||
Assertions.assertEquals(4, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
Assertions.assertEquals("from 1st children", contents[1])
|
||||
Assertions.assertEquals("from 2nd children", contents[2])
|
||||
Assertions.assertEquals("from 3rd children", contents[3])
|
||||
assertEquals(4, contents.size)
|
||||
assertEquals("from child", contents[0])
|
||||
assertEquals("from 1st children", contents[1])
|
||||
assertEquals("from 2nd children", contents[2])
|
||||
assertEquals("from 3rd children", contents[3])
|
||||
|
||||
// 使用成员类型快速读取成员对象
|
||||
contents = OmniAccessor.get(parent, "{Child}/{GrandChild}/content")
|
||||
Assertions.assertEquals(1, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
assertEquals(1, contents.size)
|
||||
assertEquals("from child", contents[0])
|
||||
|
||||
// 使用带下标的路径读取成员对象
|
||||
Assertions.assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"))
|
||||
Assertions.assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"))
|
||||
assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"))
|
||||
assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"))
|
||||
|
||||
// 使用模糊路径快速读取成员对象
|
||||
Assertions.assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"))
|
||||
assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -65,22 +66,22 @@ internal class DemoOmniMethodsTest {
|
||||
|
||||
// 使用指定路径快速给成员对象赋值
|
||||
OmniAccessor.set(parent, "child/grandChild/content", "demo child")
|
||||
Assertions.assertEquals("demo child", parent.child?.grandChild?.content)
|
||||
assertEquals("demo child", parent.child?.grandChild?.content)
|
||||
|
||||
// 使用带下标的路径给成员对象赋值
|
||||
OmniAccessor.set(parent, "children[1]/grandChild/content", "demo children[1]")
|
||||
Assertions.assertEquals("demo children[1]", parent.children?.get(1)?.grandChild?.content)
|
||||
assertEquals("demo children[1]", parent.children?.get(1)?.grandChild?.content)
|
||||
|
||||
// 使用模糊路径批量给成员对象赋值
|
||||
OmniAccessor.set(parent, "child*/*/content", "demo in batch")
|
||||
Assertions.assertEquals("demo in batch", parent.child?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(0)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(1)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(2)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.child?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(0)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(1)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(2)?.grandChild?.content)
|
||||
|
||||
// 读写私有内部类类型的成员(使用类型名引用内部类时,无需带外部类名)
|
||||
Assertions.assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
OmniAccessor.set(parent, "{InnerChild}/secret", "inner-class secret")
|
||||
Assertions.assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -15,31 +15,31 @@ internal class DemoPrivateAccessorTest {
|
||||
@Test
|
||||
fun should_access_private_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
assertEquals("abc + hello + 1", invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3)
|
||||
assertEquals(3, PrivateAccessor.get(demoPrivateAccess, "count"))
|
||||
set(demoPrivateAccess, "count", 3)
|
||||
assertEquals(3, get(demoPrivateAccess, "count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_static_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
assertEquals("hello + 1", invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_static_field() {
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
|
||||
assertEquals(3, PrivateAccessor.getStatic(DemoPrivateAccess::class.java, "staticCount"))
|
||||
setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
|
||||
assertEquals(3, getStatic(DemoPrivateAccess::class.java, "staticCount"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_update_final_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 4.13)
|
||||
set(demoPrivateAccess, "pi", 4.13)
|
||||
assertEquals(4.13, demoPrivateAccess.pi)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
@@ -42,22 +41,22 @@ internal class DemoTemplateTest {
|
||||
@Test
|
||||
fun should_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
Assertions.assertEquals("demo_mock_list", res)
|
||||
assertEquals("demo_mock_list", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_double_template_method() {
|
||||
val res = demoTemplate.doubleTemplateMethod()
|
||||
Assertions.assertEquals("testable_mock_map", res)
|
||||
assertEquals("testable_mock_map", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_new_template_method() {
|
||||
val res = demoTemplate.newTemplateMethod()
|
||||
Assertions.assertEquals(2, res.size)
|
||||
assertEquals(2, res.size)
|
||||
val iterator = res.stream().iterator()
|
||||
Assertions.assertEquals("insert_mock", iterator.next())
|
||||
Assertions.assertEquals("world_mocked", iterator.next())
|
||||
assertEquals("insert_mock", iterator.next())
|
||||
assertEquals("world_mocked", iterator.next())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@MockWith
|
||||
@@ -14,9 +14,9 @@ class OneToMultiSvcTest {
|
||||
|
||||
@Test
|
||||
fun should_test_multi_class_together() {
|
||||
Assertions.assertEquals("a_mock", aSvc.demo("test"))
|
||||
Assertions.assertEquals("b_mock", bSvc.demo("test"))
|
||||
Assertions.assertEquals("c_mock", cSvc.demo("test"))
|
||||
assertEquals("a_mock", aSvc.demo("test"))
|
||||
assertEquals("b_mock", bSvc.demo("test"))
|
||||
assertEquals("c_mock", cSvc.demo("test"))
|
||||
verify("a_format").withTimes(1)
|
||||
verify("b_format").withTimes(1)
|
||||
verify("c_format").withTimes(1)
|
||||
|
||||
2
demo/kotlin-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
omni.constructor.enhance.enable = true
|
||||
mock.target.checking.enable = true
|
||||
@@ -14,8 +14,8 @@ repositories {
|
||||
dependencies {
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:3.0.7'
|
||||
testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.1')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.8')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.8')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<testable.version>0.6.1</testable.version>
|
||||
<testable.version>0.6.8</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -51,9 +51,11 @@ In actual scenarios, there are very few scenarios where methods in the `String`
|
||||
|
||||
#### 8. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
Yes, check `demo/android-demo` project for more information.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
Please note, the `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`.
|
||||
If the test case require classes from Android SDK, it should be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
This framework will run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine, thus many Android App unit tests are written with the `Roboelectric` framework.
|
||||
|
||||
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@ Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **Note**: In the `0.6.0` version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in subsequent versions
|
||||
> **Note 1**: In the current version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in the future.
|
||||
>
|
||||
> **Note 2**: Based on the light-weight principle, in the default mode, `OmniConstructor` will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs.
|
||||
> But for more complex situations, such as when certain class have constructors throwing exceptions or contain other statements that hinder the normal execution of the construction, the object construction may fail.
|
||||
> In those situations, you can use the [Testable global configuration](en-us/doc/javaagent-args.md) `omni.constructor.enhance.enable = true` to enable bytecode enhancement mode of `OmniConstructor`, in this mode, any Java class can be constructed.
|
||||
|
||||
In addition to use as input parameters of the method under test, `OmniConstructor` can also be used to quickly construct the return value of the mock method. Compared to using `null` as the return value of the mock method, using a fully initialized object can better guarantee the reliability of the test .
|
||||
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.8
|
||||
- support `@DumpTo` annotation to dump bytecode of any transformed class
|
||||
- `PrivateAccessor.setStatic()` method now able to update static final members
|
||||
|
||||
## 0.6.7
|
||||
- support empty mock container class which has all mock methods in its parent
|
||||
- fix an issue cause mock fail when parameter has array element accessing
|
||||
|
||||
## 0.6.6
|
||||
- support global package path mapping for easier mocking code of 3rd packages
|
||||
- complete mock target checking, improve resistance to code refactoring
|
||||
- fix a log disorder issue of mock class handler
|
||||
- fix an error of creating `Long` or `Integer` class with `OmniConstructor` in certain jvm version
|
||||
|
||||
## 0.6.5
|
||||
- fix an issue cause `OmniConstructor` fail to handle classes like Date and BigDecimal
|
||||
- fix an issue of `PrivateAccessor` fail to invoke method with single array parameter
|
||||
- fix an issue of mock methods in super class are ignored when `scope` is `associated`
|
||||
- fix an issue of empty mock method not works when `scope` is `associated`
|
||||
|
||||
## 0.6.4
|
||||
- `TestableNull` class removed, let `OmniConstructor` even lighter
|
||||
- support `@Nested` annotation of JUnit 5
|
||||
- fix several `OmniConstructor` compatibility issues(thanks to [@ddatsh](https://github.com/ddatsh))
|
||||
|
||||
## 0.6.3
|
||||
- support custom inner `Mock` class name
|
||||
- support skip specified packages from `OmniConstructor` enhancement
|
||||
- fix 2 circle construction cases in `OmniConstructor`
|
||||
- fix an index out-of-range issue caused by non-static method without `this` reference
|
||||
- fix a null pointer exception caused by resource folder unavailable at runtime
|
||||
|
||||
## 0.6.2
|
||||
- support setup global testable agent configure via properties file
|
||||
- disable OmniConstructor bytecode enhancement by default
|
||||
- fix a `Spring` framework compatibility issue with OmniConstructor
|
||||
|
||||
## 0.6.1
|
||||
- generate mock scanning log file automatically, for self-troubleshooting
|
||||
- fix a `Spock` test frame compatibility issue
|
||||
- fix a `Spock` test framework compatibility issue
|
||||
- fix a `Gradle` unit test runtime compatibility issue
|
||||
|
||||
## 0.6.0
|
||||
|
||||
@@ -6,7 +6,7 @@ Use TestableMock
|
||||
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
|
||||
- [Quickly construct complicated parameter object](en-us/doc/omni-constructor.md):generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
|
||||
- [Auxiliary test void method](en-us/doc/test-void-method.md): use the mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
|
||||
- [Assist test void method](en-us/doc/test-void-method.md): use the mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
|
||||
|
||||
## Use in Maven project
|
||||
|
||||
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.6.1</testable.version>
|
||||
<testable.version>0.6.8</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.1')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.8')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.8')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ test {
|
||||
|
||||
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
|
||||
|
||||
> For Android project tested with `Robolectric` framework, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
> For Android project, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
@@ -92,13 +92,14 @@ See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
|
||||
> See project `demo/android-demo` for a complete example.
|
||||
|
||||
> If the project is using `Spock` test framework, you need to specify the bytecode version generated by `Groovy` to be 1.6 or above, the method is as follows (please modify the properties value according to the actual JVM version used).
|
||||
>
|
||||
> For Maven project, add `<maven.compiler.source>` and `<maven.compiler.target>` properties inside the `pom.xml` file, e.g.
|
||||
> ```xml
|
||||
> <properties>
|
||||
> <!-- or 1.7/1.8/... -->
|
||||
> <maven.compiler.source>1.6</maven.compiler.source>
|
||||
> <maven.compiler.target>1.6</maven.compiler.target>
|
||||
> </properties>
|
||||
@@ -106,7 +107,7 @@ See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo
|
||||
>
|
||||
> For Gradle project, add a `sourceCompatibility` property inside the `build.gradle` file, e.g.
|
||||
> ```groovy
|
||||
> sourceCompatibility = '6'
|
||||
> sourceCompatibility = '6' // or 7/8/9/...
|
||||
> ```
|
||||
>
|
||||
> See project `demo/spock-demo` for a complete example.
|
||||
|
||||
4
docs/en-us/doc/use-package-mapping.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Use Package Mapping
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
4
docs/en-us/doc/with-thread-pool.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Mock Invocation In Thread Pool
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -7,19 +7,21 @@
|
||||
|
||||
- Usage Guide
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
|
||||
- [Effective Scope Of Mocking](zh-cn/doc/scope-of-mock.md)
|
||||
- [Reusing Mock Methods](zh-cn/doc/mock-method-reusing.md)
|
||||
- [Use MockWith Annotation](zh-cn/doc/use-mock-with.md)
|
||||
- [Effective Scope Of Mocking](en-us/doc/scope-of-mock.md)
|
||||
- [Reusing Mock Methods](en-us/doc/mock-method-reusing.md)
|
||||
- [Use MockWith Annotation](en-us/doc/use-mock-with.md)
|
||||
- [Use Package Mapping](en-us/doc/use-package-mapping.md)
|
||||
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
|
||||
- [Mock Invocation In Thread Pool](zh-cn/doc/with-thread-pool.md)
|
||||
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
|
||||
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Upgrade Guide](en-us/doc/upgrade-guide.md)
|
||||
- [Available Global Arguments](zh-cn/doc/javaagent-args.md)
|
||||
- [Available Annotations](zh-cn/doc/annotations.md)
|
||||
- [How Mock Works](zh-cn/doc/design-and-mechanism.md)
|
||||
- [Available Global Arguments](en-us/doc/javaagent-args.md)
|
||||
- [Available Annotations](en-us/doc/annotations.md)
|
||||
- [How Mock Works](en-us/doc/design-and-mechanism.md)
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [About Us](en-us/doc/about-us.md)
|
||||
|
||||
@@ -58,3 +58,12 @@
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
|
||||
|
||||
#### @DumpTo
|
||||
|
||||
将指定类型被`TestableMock`处理过后的字节码导出到文件。
|
||||
|
||||
- 作用于:任意类型
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | String | 是 | N/A | 存放导出文件的目录(相对当前执行目录的相对路径),若目录不存在,将自动创建 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
问题反馈
|
||||
---
|
||||
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在24小时内回复。
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在每周末之前统一回复处理。
|
||||
|
||||
请详细描述遇到的情况,若您觉得问题可能与`TestableMock`的潜在BUG有关,请尽量提供可复现的相关源码或示例,以便进行针对性定位和排查。
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S
|
||||
|
||||
#### 8. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
可以,见`demo/android-demo`示例。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
需注意的是,Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。若测试中涉及Android SDK中的类型,请结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架使用,该框架能在普通JVM虚拟机上运行Android单元测试,且速度比通过Android虚拟机运行单元测试快非常多,目前许多Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
|
||||
@@ -1,46 +1,62 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过全局配置文件调整其执行过程。
|
||||
|
||||
### 可用参数清单
|
||||
## 1.使用全局配置文件
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
在项目的`src/test/resources`目录下创建一个名为`testable.properties`的文本文件,其中每行是一条配置项,配置项的名称与值之间用`=`连接。
|
||||
|
||||
详细的可用配置项列说明下:
|
||||
|
||||
| 配置项 | 描述 | 可用值和示例 |
|
||||
| ---- | ---- | ---- |
|
||||
| dump.path | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
|
||||
| enhance.pkgPrefix.includes | 让`TestableMock`仅在特定包内生效,通常用于大型项目 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.biz,com.3rd.biz` |
|
||||
| log.file | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
|
||||
| log.level | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| mock.innerClass.name | 修改测试类中的内置Mock容器类名 | 任意符合Java类名的值,默认值为`Mock` |
|
||||
| mock.scope.default | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| mock.target.checking.enable | 是否启用Mock目标有效性的前置检查 | 可用值为:`true` / `false`,当前默认值为`false` |
|
||||
| omni.constructor.enhance.enable | 是否启用`OmniConstructor`的字节码增强模式 | 可用值为:`true` / `false` |
|
||||
| omni.constructor.enhance.pkgPrefix.excludes | 对特定包禁用`OmniConstructor`的字节码增强模式 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.model` |
|
||||
| private.access.enhance.enable | 是否启用`PrivateAccessor`的字节码增强模式 | 可用值为:`true` / `false` |
|
||||
| thread.pool.enhance.enable | 是否启用基于`TransmittableThreadLocal`的Mock上下文存储 | 可用值为:`true` / `false` |
|
||||
|
||||
参见`demo`目录各示例项目中的`testable.properties`文件。
|
||||
|
||||
## 2.修改配置文件位置
|
||||
|
||||
除了配置文件,`TestableMock`也支持通过引入`testable-agent`包时,在末尾加上一个`=`符号,然后连接额外参数来实现全局配置。这种方法与使用配置文件基本等效,因此通常无需使用,但有唯一特例是`configFile`配置项,该配置项可用于修改全局配置文件的位置。
|
||||
|
||||
| 参数 | 描述 | 可用值和示例 |
|
||||
| ---- | ---- | ---- |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| logFile | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.svc,com.demo.dao` |
|
||||
| mockScope | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储(用于包含线程池的测试用例) | `N/A` |
|
||||
| configFile | 修改全局配置文件位置 | 可用相对路径或绝对路径,当为相对路径时,表示相对项目根目录的位置,默认值为`src/test/resources/testable.properties` |
|
||||
|
||||
### 参数的连接
|
||||
|
||||
若参数有值,参数名和值之间用`=`符合连接。例如:
|
||||
|
||||
`useThreadPool`、`logLevel=debug`、`dumpPath=/tmp/debug`
|
||||
|
||||
多个参数之间使用`&`符号连接,例如:
|
||||
|
||||
`useThreadPool&logLevel=debug`、`logLevel=debug&dumpPath=/tmp/debug`
|
||||
|
||||
### 添加运行参数
|
||||
|
||||
与其他基于JavaAgent的工具相似,`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如将配置文件位置修改为项目根路径下的`config/testable.properties`文件:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo.</argLine>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=configFile=config/testable.properties</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&`
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=configFile=config/testable.properties"
|
||||
```
|
||||
|
||||
## 3. 全局配置技巧
|
||||
|
||||
对于一般的小型项目,`TestableMock`的所有功能都是开箱即用的,正确引用依赖包后,无需进行额外配置。
|
||||
|
||||
默认情况下,`TestableMock`会在构建目录中(Maven构建的`target`目录或Gradle构建的`build`目录)自动生成一个记录执行过程的`testable-agent.log`文件。若希望禁用此日志文件,可将`log.file`参数赋值为`null`。
|
||||
|
||||
若测试中使用了`OmniConstructor`且遇到构造出错的情况,可开启`omni.constructor.enhance.enable`配置(倘若开启后依然报错,请提Issue告诉我们)。`omni.constructor.enhance.pkgPrefix.excludes`配置主要用于当开启`OmniConstructor`字节码增强模式报错时,临时绕过某些无法处理的类型,通常无需使用。
|
||||
|
||||
若项目较大(构建生成的jar包大于100MB),可考虑使用`enhance.pkgPrefix.includes`参数来减少`TestableMock`在测试启动前建立Mock关联和进行`OmniConstructor`预处理的扫描时长,从而加快单元测试启动速度。通常将值设置为当前项目自身的`<group>.<artifact>`路径即可,如需Mock三方包中的调用,或通过`OmniConstructor`构造来自三方包中的类型,则还应该加上相应的三方包路径。
|
||||
|
||||
若项目测试中,既包含真实的单元测试,又包含了使用单元测试框架编写的集成测试时。为了让集成测试的执行过程不受Mock影响,可能需要使用`mock.scope.default`将默认的Mock方法范围限制为仅对所属类型的单元测试用例生效。
|
||||
|
||||
若需Mock的调用发生在线程池中,且遇到`verify()`结果或`MOCK_CONTEXT`内容不正确的时候,则需考虑开启`thread.pool.enhance.enable`配置,详见[Mock线程池内的调用](zh-cn/doc/with-thread-pool.md)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
复用Mock类与方法
|
||||
---
|
||||
|
||||
Don't Repeat Yourself是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
“Don't Repeat Yourself”是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
|
||||
TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>复用Mock方法</u>。
|
||||
|
||||
@@ -11,7 +11,6 @@ TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>
|
||||
|
||||
进行类级别的Mock复用,只需将Mock容器定义为独立的类,然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
|
||||
|
||||
|
||||
```java
|
||||
@MockWith(ServiceMock.class)
|
||||
public class AaaServiceTest {
|
||||
|
||||
@@ -35,7 +35,11 @@ Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本中修复
|
||||
> **注意 1** :在当前版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本修复
|
||||
>
|
||||
> **注意 2** :基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象,对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
|
||||
> 但对于更复杂的情形,譬如遇到某些类型的构造方法会抛出异常或包含其他妨碍构造正常执行的语句时,对象构造可能会失败。
|
||||
> 此时可通过[Testable全局配置](zh-cn/doc/javaagent-args.md)`omni.constructor.enhance.enable = true`启用`OmniConstructor`的字节码增强模式,在该模式下,任何Java类型皆可构造。
|
||||
|
||||
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值,相比将`null`作为Mock方法的返回值,使用完全初始化的对象能够更好保障测试的可靠性。
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
|
||||
### 2.1 直接访问私有成员
|
||||
|
||||
> 由于IDE语法报错原因,此特性计划在未来版本中移除,建议采用`PrivateAccessor`方式
|
||||
|
||||
第二种方法,除了借助`PrivateAccessor`工具类以外,凡是使用了`@EnablePrivateAccess`注解的测试类还会被自动赋予以下“特殊能力”:
|
||||
|
||||
- 调用**被测类**的私有方法(包括静态方法)
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.8
|
||||
- 支持使用`@DumpTo`注解导出任意类处理后的字节码
|
||||
- 支持使用`PrivateAccessor.setStatic()`方法修改静态常量成员
|
||||
|
||||
## 0.6.7
|
||||
- 支持所有Mock方法都定义在父类中的空Mock容器类(issue-167)
|
||||
- 修复一处导致参数包含数组下标读取的调用无法Mock的问题(issue-171)
|
||||
|
||||
## 0.6.6
|
||||
- 支持全局参数配置Mock容器的包路径映射,便于Mock三方包内的代码(issue-105)
|
||||
- 实现Mock功能的防代码重构机制(issue-5)
|
||||
- 解决Mock类的调试日志内容有时会错位的问题
|
||||
- 修复`OmniConstructor`在某些版本JVM下构造`Long`和`Integer`类报错的问题
|
||||
|
||||
## 0.6.5
|
||||
- 修复`OmniConstructor`对某些系统类型的兼容问题(issue-145)
|
||||
- 修复`PrivateAccessor`无法调用仅有数组类型参数方法的问题(issue-152)
|
||||
- 修复当`scope`为`associated`时父类中的Mock方法不生效问题(issue-139)
|
||||
- 修复当`scope`为`associated`时空白的Mock方法会报错的问题(issue-154)
|
||||
|
||||
## 0.6.4
|
||||
- 移除`TestableNull`类型,让`OmniConstructor`更轻量
|
||||
- 支持JUnit 5的`@Nested`注解(issue-140)
|
||||
- 修复多处`OmniConstructor`的兼容性BUG(感谢[@ddatsh](https://github.com/ddatsh))
|
||||
|
||||
## 0.6.3
|
||||
- 支持自定义内部Mock容器类名(issue-137)
|
||||
- 支持自定义`OmniConstructor`字节码增强范围
|
||||
- 修复两处可导致`OmniConstructor`无限递归构建的问题(包括issue-138)
|
||||
- 修复一处非静态方法获取`this`引用失败导致的下标越界异常(issue-136)
|
||||
- 修复获取构建目录失败导致的空指针异常(issue-135)
|
||||
|
||||
## 0.6.2
|
||||
- 支持通过Properties文件配置TestableAgent
|
||||
- 默认禁用OmniConstructor相关的字节码增强
|
||||
- 修复OmniConstructor与`Spring`框架的兼容问题(issue-129)
|
||||
|
||||
## 0.6.1
|
||||
- 自动生成Mock扫描过程日志文件,便于自助排查问题
|
||||
- 修复一处`Spock`测试框架的兼容问题 (issue-121)
|
||||
@@ -115,26 +152,26 @@
|
||||
- 重名了现有的各种注解,以便与更好的与实际功能对应
|
||||
|
||||
## v0.1.0
|
||||
- move generated agent jar to class folder
|
||||
- support mock method of any object
|
||||
- 将生成的Agent Jar包自动移到打包工程的Class目录
|
||||
- 支持Mock任意类型的方法调用(不再局限于被测类中的方法)
|
||||
|
||||
## v0.0.5
|
||||
- use dynamically runtime modification to replace static `e.java` file
|
||||
- get rid of unit test framework dependence
|
||||
- add testable ref field in test class at runtime instead of compile time
|
||||
- 使用运行期字节码修改替代预先生成的`e.java`文件
|
||||
- 消除所有对特定单元测试框架的定制逻辑
|
||||
- 将测试类中的Testable引用字段从编译期生成改为运行期生成
|
||||
|
||||
## v0.0.4
|
||||
- use runtime byte code rewrite to invoke testable setup method
|
||||
- add `TestableUtil` class to fetch current test case and invocation source
|
||||
- 改用运行期字节码修改来调用Testable的`setup`方法
|
||||
- 添加`TestableUtil`工具类用于获取当前测试用例和调用来源
|
||||
|
||||
## v0.0.3
|
||||
- use global method invoke to access private members instead of modification in place
|
||||
- use `e.java` replace `testable` class make code more readable
|
||||
- introduce `agent` module, use runtime byte code modification to support new operation and member method mocking
|
||||
- 使用静态方法封装私有成员的访问过程,不在原调用处直接添加反射代码
|
||||
- 使用`e.java`文件替代`testable`类型,提高代码可读性
|
||||
- 引入`agent`模块,使用运行期字节码修改实现`new`操作和私有成员调用的Mock
|
||||
|
||||
## v0.0.2
|
||||
- add support of member method mocking by compile time code modification
|
||||
- 基于编译期代码修改支持Mock私有成员方法的调用
|
||||
|
||||
## v0.0.1
|
||||
- PoC version
|
||||
- use compile time code modification to support new operation mocking and private field & method access
|
||||
- 首个概念验证版本
|
||||
- 基于编译期代码修改支持`new`操作的Mock和访问被测类私有成员
|
||||
|
||||
@@ -6,11 +6,15 @@ Mock的生效范围
|
||||
- `MockScope.GLOBAL`:该Mock方法将全局生效
|
||||
- `MockScope.ASSOCIATED`:该Mock方法仅对Mock容器关联测试类中的测试用例生效
|
||||
|
||||
举例来说,`AaaService`和`BbbService`是两个需要被测试的类,在`BbbService`的代码里有个`recordTicket()`调用依赖外部系统。因此在进行单元测试时,开发者在`BbbService`关联的Mock容器里使用`@MockMethod`注解定义了这个调用的替代方法。此时,若该Mock方法的`scope`值为`MockScope.GLOBAL`,则不论是在`AaaServiceTest`测试类还是在`BbbServiceTest`测试类的测试用例,只要直接或间接的执行到这行调用,都会被置换为调用Mock方法。若该Mock方法的`scope`值为`MockScope.ASSOCIATED`,则Mock只对`BbbServiceTest`类中的测试用例生效,而`AaaServiceTest`类中的测试用例在运行过程中执行到了`BbbService`类的相关代码,将会执行`recordTicket()`的原本调用。
|
||||
|
||||
对于常规项目而言,单元测试里需要被Mock的调用都是由于其中包含了不需要或不便于测试的逻辑,譬如“依赖外部系统”、“包含随机结果”、“执行非常耗时”等等,这类调用在整个单元测试的生命周期里都应该被Mock方法置换,不论调用的发起者是谁。因此`TestableMock`默认所有Mock方法都是全局生效的,即`scope`默认值为`MockScope.GLOBAL`。
|
||||
|
||||
在一些大型项目中,会有“下层模块编写单元测试,上层模块编写端到端集成测试,两者混合在一起运行”的情况,这时候大部分Mock方法都应该使用`MockScope.ASSOCIATED`作为生效范围。针对这种情况,`TestableMock`支持通过`mockScope`运行参数来修改默认的Mock方法生效范围,详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。
|
||||
> 举例来说,`CookerService`和`SellerService`是两个需要被测试的类,假设`CookerService`的代码里的`hireXxx()`和`cookXxx()`方法都需要依赖外部系统。因此在进行单元测试时,开发者在`CookerService`关联的Mock容器里使用`@MockMethod`注解定义了这些调用的替代方法。
|
||||
>
|
||||
> 此时,若该Mock方法的`scope`值为`MockScope.GLOBAL`,则不论是在`SellerServiceTest`测试类还是在`CookerServiceTest`测试类的测试用例,只要直接或间接的执行到这行调用,都会被置换为调用Mock方法。若该Mock方法的`scope`值为`MockScope.ASSOCIATED`,则Mock只对`CookerServiceTest`类中的测试用例生效,而`SellerServiceTest`类中的测试用例在运行过程中执行到了`CookerService`类的相关代码,将会执行原本的调用。
|
||||
>
|
||||
> 参见Java和Kotlin示例中`SellerServiceTest`测试类的用例。
|
||||
|
||||
在一些大型项目中,会有“下层模块编写单元测试,上层模块编写端到端集成测试,两者混合在一起运行”的情况,这时候大部分Mock方法都应该使用`MockScope.ASSOCIATED`作为生效范围。针对这种情况,`TestableMock`支持通过`mock.scope.default`运行参数来修改默认的Mock方法生效范围,详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。
|
||||
|
||||
> 特别说明。若要Mock静态块里的调用,Mock方法的`scope`必须为`MockScope.GLOBAL`,因为静态块中的代码在程序初始化时就会执行,不属于任何测试用例。典型场景是在使用JNI开发的项目中Mock系统库的加载方法。
|
||||
> ```java
|
||||
@@ -24,4 +28,4 @@ Mock的生效范围
|
||||
> private void loadLibrary(String libname) {
|
||||
> System.err.println("loadLibrary " + libname);
|
||||
> }
|
||||
> ```
|
||||
> ```
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.6.1</testable.version>
|
||||
<testable.version>0.6.8</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.1')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.8')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.8')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ test {
|
||||
|
||||
参见项目`java-demo`的[build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle)和`kotlin-demo`的[build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts)文件。
|
||||
|
||||
> 若是基于`Robolectric`框架的Android项目,则添加`TestableMock`依赖方法同上,添加javaagent配置方法如下:
|
||||
> 若用于Android项目,则添加`TestableMock`依赖方法同上,添加javaagent配置方法如下:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
@@ -92,13 +92,14 @@ test {
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
|
||||
> 完整代码可参考`demo/android-demo`示例项目。
|
||||
|
||||
> 若项目使用`Spock`测试框架,需指定`Groovy`编译生成的JVM 1.6或以上版本字节码,方法如下(请根据实际使用的JVM版本修改属性值)。
|
||||
> 若项目使用`Spock`测试框架,需指定`Groovy`编译生成的JVM **1.6或以上**版本字节码,方法如下(请根据实际使用的JVM版本修改属性值)。
|
||||
>
|
||||
> Maven项目在`pom.xml`中添加`<maven.compiler.source>`和`<maven.compiler.target>`属性,例如:
|
||||
> ```xml
|
||||
> <properties>
|
||||
> <!-- 或 1.7/1.8/... -->
|
||||
> <maven.compiler.source>1.6</maven.compiler.source>
|
||||
> <maven.compiler.target>1.6</maven.compiler.target>
|
||||
> </properties>
|
||||
@@ -106,7 +107,7 @@ test {
|
||||
>
|
||||
> Gradle项目在`build.gradle`中添加`sourceCompatibility`属性,例如:
|
||||
> ```groovy
|
||||
> sourceCompatibility = '6'
|
||||
> sourceCompatibility = '6' // 或7/8/9/...
|
||||
> ```
|
||||
>
|
||||
> 完整代码可参考`demo/spock-demo`示例项目。
|
||||
|
||||
@@ -88,10 +88,4 @@ public class ServiceTest {
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中`OneToMultiSvcTest`测试类的用例。
|
||||
|
||||
> 由于当前版本里,测试类无法通过`@MockWith`与多个Mock容器关联,目前这种用法仅支持生效范围为`MockScope.GLOBAL`的Mock方法,这是已知BUG,将在未来版本中修复。
|
||||
|
||||
### 4. 使用不包含Mock方法的Mock容器类
|
||||
|
||||
为了加快搜索Mock容器类的速度,在扫描过程中,`TestableMock`只会将自身定义有Mock方法(包含`@MockMethod`或`@MockMockConstructor`注解的方法)以及明确被`@MockWith`指向的类识别为有效的Mock容器,而不会去遍历其父类。
|
||||
|
||||
假如出于某些极特殊原因要使用无Mock方法的类型作为Mock容器,譬如希望将实际Mock方法均定义在父类,实际使用的子容器仅仅重载父类的某些特定方法。此时即使Mock容器类的位置符合约定,为了能够被识别,依然应该在相应的测试类上增加对Mock容器类的`@MockWith`引用。
|
||||
> 由于测试类无法通过`@MockWith`与多个Mock容器关联,目前这种用法仅支持生效范围为`MockScope.GLOBAL`的Mock方法。
|
||||
|
||||
84
docs/zh-cn/doc/use-package-mapping.md
Normal file
@@ -0,0 +1,84 @@
|
||||
使用包路径映射
|
||||
---
|
||||
|
||||
虽然在规范的单元测试中,测试编写者应当关注于被测类自身的业务逻辑,将无关外部调用按需替换为Mock,然而现实中的单元测试通常总是会调用到一部分外部类的逻辑。
|
||||
|
||||
`TestableMock`基于"让每个业务类提供自己的Mock方法集合"的设计原则,默认约定包含Mock方法集合的容器类型应与业务类具有相同的包路径,否则需显式的[使用MockWith注解](zh-cn/doc/use-mock-with.md)。当遇到需Mock的逻辑属于某个三方包时,由于无法直接修改代码添加`@MockWith`注解,为了符合包路径约定,会导致在测试代码里出现孤立的"飞包"现象:
|
||||
|
||||
```
|
||||
src
|
||||
├── main
|
||||
│ └── java
|
||||
│ └── com
|
||||
│ └── demo
|
||||
│ └── biz
|
||||
│ ├── service
|
||||
│ │ ├── AaService.java
|
||||
│ │ ├── BbService.java
|
||||
│ │ ...
|
||||
│ ├── util
|
||||
│ │ └── ToolUtil.java
|
||||
│ ...
|
||||
└── test
|
||||
└── java
|
||||
└── com
|
||||
├── demo
|
||||
│ └── biz
|
||||
│ ├── service
|
||||
│ │ ├── AaServiceTest.java
|
||||
│ │ ├── BbServiceTest.java
|
||||
│ │ ...
|
||||
│ ├── util
|
||||
│ │ └── ToolUtilTest.java
|
||||
│ ...
|
||||
└── 3rd
|
||||
└── party
|
||||
└── pkg
|
||||
└── SomethingMock.java <- 孤立的"飞包"
|
||||
```
|
||||
|
||||
包路径映射功能就是用来解决这个问题的。在[`testable.properties`配置文件](zh-cn/doc/javaagent-args.md)中,添加以`mock.package.mapping`开头的配置项,具体格式为:
|
||||
|
||||
```properties
|
||||
mock.package.mapping.<业务类所在包路径> = <Mock类所在包路径>
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```properties
|
||||
mock.package.mapping.com.3rd.party.pkg = com.demo.biz.3rd
|
||||
```
|
||||
|
||||
此时若需要Mock的代码位于`com.3rd.party.pkg`下的`Something`类型里,则只需在测试目录的`com.demo.biz.3rd`包中创建`SomethingMock`类型,然后在其中添加需Mock的目标方法即可。
|
||||
|
||||
使用映射后的测试包目录结构如下,"飞包"问题不复存在了。
|
||||
|
||||
```
|
||||
src
|
||||
├── main
|
||||
│ └── ...
|
||||
└── test
|
||||
└── java
|
||||
└── com
|
||||
├── demo
|
||||
│ └── biz
|
||||
│ ├── 3rd
|
||||
│ │ └── SomethingMock.java <- 三方包Mock容器类
|
||||
│ ├── service
|
||||
│ │ ├── AaServiceTest.java
|
||||
│ │ ├── BbServiceTest.java
|
||||
│ │ ...
|
||||
│ ├── util
|
||||
│ │ └── ToolUtilTest.java
|
||||
│ ...
|
||||
```
|
||||
|
||||
可根据实际情况,在配置文件中添加任意多条不同的路径映射。
|
||||
|
||||
> 注意:上述示例中的`3rd`是非法的Java包路径名称,仅用于功能介绍目的
|
||||
|
||||
对于包映射功能的几项提醒:
|
||||
|
||||
- 对三方包使用范围为`ASSOCIATED`的Mock方法没有任何意义(该方法将永远不会生效),请使用`scope`为`GLOBAL`的Mock方法
|
||||
- 虽然映射后的路径同样支持用测试类中的`Mock`内部类作为Mock容器,但通常更建议直接使用`<业务类>+Mock`方式命名的独立Mock容器类,以便于代码阅读
|
||||
- 在多模块的Maven或Gradle工程中,由于模块之间是Jar包集成,若需要进行跨模块Mock,应在测试用例所在模块里提供单独的Mock容器类,并根据情况决定是否需要进行包路径映射
|
||||
52
docs/zh-cn/doc/with-thread-pool.md
Normal file
@@ -0,0 +1,52 @@
|
||||
Mock线程池内的调用
|
||||
---
|
||||
|
||||
`TestableMock`采用来自[transmittable-thread-local](https://github.com/alibaba/transmittable-thread-local)项目的`TransmittableThreadLocal`类型存储测试用例运行期的`MOCK_CONTEXT`内容和Mock方法调用过程。
|
||||
|
||||
当线程池中的执行对象未经过`TtlRunnable`或`TtlCallable`处理时,`TransmittableThreadLocal`将自动降级为与`InheritableThreadLocal`等效的类型,即只对父子线程有效,无法在线程池上下文中正常传递存储数据。因而会导致`MOCK_CONTEXT`内容丢失和`verify()`方法校验结果不正确的情况。
|
||||
|
||||
为此,可以启用[Testable全局配置](zh-cn/doc/javaagent-args.md)`thread.pool.enhance.enable=true`,来自动在测试启动时自动封装程序中的普通`Runnable`和`Callable`对象,使`TransmittableThreadLocal`恢复跨线程池存储数据的能力。
|
||||
|
||||
同时还需要配合修改项目`pom.xml`或`build.gradle`文件,将`transmittable-thread-local`中的类型增加到`TestableMock`运行的Classpath里,具体方法如下。
|
||||
|
||||
### 使用Maven构建
|
||||
|
||||
首先增加一个属性,以便将来修改该依赖的版本值:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<transmittable.thread.local.version>2.12.1</transmittable.thread.local.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
然后,在`maven-surefire-plugin`插件的`argLine`参数中添加运行参数`-Xbootclasspath/a:${settings.localRepository}/com/alibaba/transmittable-thread-local/${transmittable.thread.local.version}/transmittable-thread-local-${transmittable.thread.local.version}.jar`。
|
||||
|
||||
添加后的完整`maven-surefire-plugin`配置参考:
|
||||
|
||||
```xml
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-Xbootclasspath/a:${settings.localRepository}/com/alibaba/transmittable-thread-local/${transmittable.thread.local.version}/transmittable-thread-local-${transmittable.thread.local.version}.jar -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
### 使用Gradle构建
|
||||
|
||||
在`test`区块内添加测试参数`jvmArgs "-Xbootclasspath/a:${classpath.find { it.name.contains("transmittable-thread-local") }.absolutePath}"`
|
||||
|
||||
添加后的完整`test`区块配置参考:
|
||||
|
||||
```groovy
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
jvmArgs "-Xbootclasspath/a:${classpath.find { it.name.contains("transmittable-thread-local") }.absolutePath}"
|
||||
... // 其他测试配置
|
||||
}
|
||||
```
|
||||
@@ -10,7 +10,9 @@
|
||||
- [Mock的生效范围](zh-cn/doc/scope-of-mock.md)
|
||||
- [复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)
|
||||
- [使用MockWith注解](zh-cn/doc/use-mock-with.md)
|
||||
- [使用包路径映射](zh-cn/doc/use-package-mapping.md)
|
||||
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
|
||||
- [Mock线程池内的调用](zh-cn/doc/with-thread-pool.md)
|
||||
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)
|
||||
- [自助问题排查](zh-cn/doc/troubleshooting.md)
|
||||
- [使用Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<version>0.6.8</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
package com.alibaba.testable.agent;
|
||||
|
||||
import com.alibaba.testable.agent.config.ArgumentParser;
|
||||
import com.alibaba.testable.agent.config.PropertiesParser;
|
||||
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.ttl.threadpool.agent.TtlAgent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR;
|
||||
|
||||
/**
|
||||
* Agent entry, dynamically modify the byte code of classes under testing
|
||||
* @author flin
|
||||
*/
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String USE_THREAD_POOL = "useThreadPool";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String LOG_FILE = "logFile";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String MOCK_SCOPE = "mockScope";
|
||||
private static final String EQUAL = "=";
|
||||
private static boolean enhanceThreadLocal = false;
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
parseArgs(agentArgs);
|
||||
ArgumentParser.parseArgs(agentArgs);
|
||||
PropertiesParser.parseFile(ArgumentParser.configFilePath);
|
||||
GlobalConfig.setupLogRootPath();
|
||||
if (enhanceThreadLocal) {
|
||||
LogUtil.info("TestableMock start at %s", System.getProperty(PROPERTY_USER_DIR));
|
||||
if (GlobalConfig.enhanceThreadLocal) {
|
||||
// add transmittable thread local transformer
|
||||
TtlAgent.premain(agentArgs, inst);
|
||||
}
|
||||
@@ -36,36 +31,6 @@ public class PreMain {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private static void parseArgs(String args) {
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
// parameter with key = value
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(LOG_FILE)) {
|
||||
GlobalConfig.setLogFile(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefix(v);
|
||||
} else if (k.equals(MOCK_SCOPE)) {
|
||||
GlobalConfig.setDefaultMockScope(MockScope.of(v));
|
||||
}
|
||||
} else {
|
||||
// parameter with single value
|
||||
if (a.equals(USE_THREAD_POOL)) {
|
||||
enhanceThreadLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanup() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.alibaba.testable.agent.config;
|
||||
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
|
||||
public class ArgumentParser {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String USE_THREAD_POOL = "useThreadPool";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String LOG_FILE = "logFile";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String MOCK_SCOPE = "mockScope";
|
||||
private static final String CONFIG_FILE = "configFile";
|
||||
private static final String EQUAL = "=";
|
||||
public static String configFilePath = null;
|
||||
|
||||
public static void parseArgs(String args) {
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
// parameter with key = value
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(LOG_FILE)) {
|
||||
GlobalConfig.setLogFile(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefixWhiteList(v);
|
||||
} else if (k.equals(MOCK_SCOPE)) {
|
||||
GlobalConfig.defaultMockScope = MockScope.of(v);
|
||||
} else if (k.equals(CONFIG_FILE)) {
|
||||
configFilePath = v;
|
||||
}
|
||||
} else {
|
||||
// parameter with single value
|
||||
if (a.equals(USE_THREAD_POOL)) {
|
||||
GlobalConfig.enhanceThreadLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.alibaba.testable.agent.config;
|
||||
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.agent.util.PathUtil;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Properties;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR;
|
||||
|
||||
public class PropertiesParser {
|
||||
|
||||
private static final String DEFAULT_CONFIG_FILE = "src/test/resources/testable.properties";
|
||||
private static final String DUMP_PATH = "dump.path";
|
||||
private static final String PKG_PREFIX_EXCLUDES = "enhance.pkgPrefix.excludes";
|
||||
private static final String PKG_PREFIX_INCLUDES = "enhance.pkgPrefix.includes";
|
||||
private static final String LOG_FILE = "log.file";
|
||||
private static final String LOG_LEVEL = "log.level";
|
||||
private static final String ENABLE_MOCK_INJECT = "mock.enhance.enable";
|
||||
private static final String INNER_MOCK_CLASS_NAME = "mock.innerClass.name";
|
||||
private static final String MOCK_PKG_MAPPING_PREFIX = "mock.package.mapping.";
|
||||
private static final String DEFAULT_MOCK_SCOPE = "mock.scope.default";
|
||||
private static final String ENABLE_MOCK_TARGET_CHECK = "mock.target.checking.enable";
|
||||
private static final String ENABLE_OMNI_INJECT = "omni.constructor.enhance.enable";
|
||||
private static final String ENABLE_FINAL_INJECT = "private.access.enhance.enable";
|
||||
private static final String ENABLE_THREAD_POOL = "thread.pool.enhance.enable";
|
||||
|
||||
public static void parseFile(String configFilePath) {
|
||||
String path = (configFilePath == null) ? DEFAULT_CONFIG_FILE : configFilePath;
|
||||
String fullPath = PathUtil.isAbsolutePath(path) ? path :
|
||||
PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path);
|
||||
Properties pps = new Properties();
|
||||
try {
|
||||
InputStream in = new BufferedInputStream(new FileInputStream(fullPath));
|
||||
pps.load(in);
|
||||
LogUtil.verbose("Loaded configure file %s", fullPath);
|
||||
} catch (IOException e) {
|
||||
if (configFilePath == null) {
|
||||
LogUtil.verbose("No configure file found, skip.");
|
||||
} else {
|
||||
LogUtil.warn("No configure file found at %s", fullPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
parsePropertiesContent(pps);
|
||||
}
|
||||
|
||||
private static void parsePropertiesContent(Properties pps) {
|
||||
Enumeration<?> en = pps.propertyNames();
|
||||
while(en.hasMoreElements()) {
|
||||
String k = (String)en.nextElement();
|
||||
String v = pps.getProperty(k);
|
||||
if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX_EXCLUDES)) {
|
||||
GlobalConfig.setPkgPrefixBlackList(v);
|
||||
} else if (k.equals(PKG_PREFIX_INCLUDES)) {
|
||||
GlobalConfig.setPkgPrefixWhiteList(v);
|
||||
} else if (k.equals(LOG_FILE)) {
|
||||
GlobalConfig.setLogFile(v);
|
||||
} else if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(INNER_MOCK_CLASS_NAME)) {
|
||||
GlobalConfig.innerMockClassName = v;
|
||||
} else if (k.startsWith(MOCK_PKG_MAPPING_PREFIX)) {
|
||||
GlobalConfig.addMockPackageMapping(k.substring(MOCK_PKG_MAPPING_PREFIX.length()), v);
|
||||
} else if (k.equals(DEFAULT_MOCK_SCOPE)) {
|
||||
GlobalConfig.defaultMockScope = MockScope.of(v);
|
||||
} else if (k.equals(ENABLE_OMNI_INJECT)) {
|
||||
GlobalConfig.enhanceOmniConstructor = Boolean.parseBoolean(v);
|
||||
} else if (k.equals(ENABLE_MOCK_INJECT)) {
|
||||
GlobalConfig.enhanceMock = Boolean.parseBoolean(v);
|
||||
} else if (k.equals(ENABLE_FINAL_INJECT)) {
|
||||
GlobalConfig.enhanceFinal = Boolean.parseBoolean(v);
|
||||
} else if (k.equals(ENABLE_MOCK_TARGET_CHECK)) {
|
||||
GlobalConfig.checkMockTargetExistence = Boolean.parseBoolean(v);
|
||||
} else if (k.equals(ENABLE_THREAD_POOL)) {
|
||||
GlobalConfig.enhanceThreadLocal = Boolean.parseBoolean(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,10 @@ public class ConstPool {
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
public static final String FIELD_TARGET_CLASS = "targetClass";
|
||||
public static final String FIELD_SCOPE = "scope";
|
||||
public static final String FIELD_VALUE = "value";
|
||||
|
||||
public static final String PROPERTY_USER_DIR = "user.dir";
|
||||
public static final String PROPERTY_TEMP_DIR = "java.io.tmpdir";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String DUMP_TO = "com.alibaba.testable.core.annotation.DumpTo";
|
||||
@@ -21,4 +23,5 @@ public class ConstPool {
|
||||
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
|
||||
public static final String KOTLIN_PREFIX_ACCESS = "access$";
|
||||
|
||||
public static final String CLASS_OBJECT = "java/lang/Object";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldNode;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class FinalFieldClassHandler extends BaseClassHandler {
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if ((cn.access & ACC_INTERFACE) == 0) {
|
||||
for (FieldNode field : cn.fields) {
|
||||
field.access &= ~ACC_FINAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ByteCodeConst;
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.*;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.util.MockAssociationUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.*;
|
||||
@@ -13,7 +14,7 @@ import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_ARRAY;
|
||||
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_CLASS;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.CLASS_OBJECT;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
|
||||
/**
|
||||
@@ -41,9 +42,16 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
LogUtil.diagnose("Found mock class %s", cn.name);
|
||||
if (!CLASS_OBJECT.equals(cn.superName)) {
|
||||
MockAssociationUtil.recordSubMockContainer(ClassUtil.toDotSeparatedName(cn.superName),
|
||||
ClassUtil.toDotSeparatedName(cn.name));
|
||||
}
|
||||
injectRefFieldAndGetInstanceMethod(cn);
|
||||
int mockMethodCount = 0;
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (isMockMethod(mn)) {
|
||||
mockMethodCount++;
|
||||
mn.access = BytecodeUtil.toPublicAccess(mn.access);
|
||||
// firstly, unfold target class from annotation to parameter
|
||||
unfoldTargetClass(mn);
|
||||
@@ -55,6 +63,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
handleTestableUtil(mn);
|
||||
}
|
||||
}
|
||||
LogUtil.diagnose(" Found %d mock methods", mockMethodCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +110,8 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
// must get label before method description changed
|
||||
ImmutablePair<LabelNode, LabelNode> labels = getStartAndEndLabel(mn);
|
||||
mn.desc = MethodUtil.addParameterAtBegin(mn.desc, targetClassName);
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : 1;
|
||||
// in certain case, local variable table of non-static method can be empty (issue-136)
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : Math.min(mn.localVariables.size(), 1);
|
||||
mn.localVariables.add(parameterOffset, new LocalVariableNode(SELF_REF, targetClassName, null,
|
||||
labels.left, labels.right, parameterOffset));
|
||||
for (int i = parameterOffset + 1; i < mn.localVariables.size(); i++) {
|
||||
@@ -123,7 +133,8 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
}
|
||||
|
||||
private ImmutablePair<LabelNode, LabelNode> getStartAndEndLabel(MethodNode mn) {
|
||||
if (MethodUtil.isStatic(mn)) {
|
||||
// in certain case, local variable table of non-static method can be empty (issue-136)
|
||||
if (MethodUtil.isStatic(mn) || mn.localVariables.isEmpty()) {
|
||||
LabelNode startLabel = null, endLabel = null;
|
||||
for (AbstractInsnNode n = mn.instructions.getFirst(); n != null; n = n.getNext()) {
|
||||
if (n instanceof LabelNode) {
|
||||
@@ -163,6 +174,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
il.add(invokeOriginalMethod(mn));
|
||||
il.add(firstLine);
|
||||
il.add(new FrameNode(F_SAME, 0, null, 0, null));
|
||||
mn.maxStack = Math.max(6, mn.maxStack);
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
}
|
||||
|
||||
@@ -216,7 +228,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (isMockMethodAnnotation(an) || isMockConstructorAnnotation(an)) {
|
||||
MockScope scope = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_SCOPE,
|
||||
GlobalConfig.getDefaultMockScope(), MockScope.class);
|
||||
GlobalConfig.defaultMockScope, MockScope.class);
|
||||
if (scope.equals(MockScope.GLOBAL)) {
|
||||
return true;
|
||||
}
|
||||
@@ -230,15 +242,41 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
return false;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (isMockMethodAnnotation(an) && AnnotationUtil.isValidMockMethod(mn, an)) {
|
||||
if (isMockMethodAnnotation(an)) {
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
|
||||
getTargetMethodOwner(mn, an), getTargetMethodName(mn, an), getTargetMethodDesc(mn, an)));
|
||||
}
|
||||
return true;
|
||||
} else if (isMockConstructorAnnotation(an)) {
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(" Mock constructor \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
|
||||
ClassUtil.toJavaStyleClassName(MethodUtil.getReturnType(mn.desc)), mn.desc));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getTargetMethodOwner(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
|
||||
null, Type.class);
|
||||
return type == null ? MethodUtil.getFirstParameter(mn.desc) : type.getClassName();
|
||||
}
|
||||
|
||||
private String getTargetMethodName(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
String name = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_METHOD,
|
||||
null, String.class);
|
||||
return name == null ? mn.name : name;
|
||||
}
|
||||
|
||||
private String getTargetMethodDesc(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
|
||||
null, Type.class);
|
||||
return type == null ? MethodUtil.removeFirstParameter(mn.desc) : mn.desc;
|
||||
}
|
||||
|
||||
private boolean isMockConstructorAnnotation(AnnotationNode an) {
|
||||
return ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc);
|
||||
}
|
||||
@@ -265,13 +303,13 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
InsnList il = new InsnList();
|
||||
List<Byte> types = MethodUtil.getParameterTypes(mn.desc);
|
||||
int size = types.size();
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
il.add(BytecodeUtil.getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, CLASS_OBJECT));
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : 1;
|
||||
for (int i = 0; i < size; i++) {
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
|
||||
il.add(BytecodeUtil.getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = BytecodeUtil.getLoadParameterByteCode(types.get(i));
|
||||
il.add(new VarInsnNode(code.left, parameterOffset));
|
||||
parameterOffset += code.right;
|
||||
MethodInsnNode typeConvertMethodNode = ClassUtil.getPrimaryTypeConvertMethod(types.get(i));
|
||||
@@ -285,7 +323,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String annotationName = toJavaStyleClassName(an.desc);
|
||||
String annotationName = ClassUtil.toJavaStyleClassName(an.desc);
|
||||
if (ConstPool.MOCK_CONSTRUCTOR.equals(annotationName)) {
|
||||
return true;
|
||||
} else if (ConstPool.MOCK_METHOD.equals(annotationName)) {
|
||||
@@ -299,42 +337,4 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ByteCodeConst.TYPE_BYTE:
|
||||
case ByteCodeConst.TYPE_CHAR:
|
||||
case ByteCodeConst.TYPE_SHORT:
|
||||
case ByteCodeConst.TYPE_INT:
|
||||
case ByteCodeConst.TYPE_BOOL:
|
||||
return ImmutablePair.of(ILOAD, 1);
|
||||
case ByteCodeConst.TYPE_DOUBLE:
|
||||
return ImmutablePair.of(DLOAD, 2);
|
||||
case ByteCodeConst.TYPE_FLOAT:
|
||||
return ImmutablePair.of(FLOAD, 1);
|
||||
case ByteCodeConst.TYPE_LONG:
|
||||
return ImmutablePair.of(LLOAD, 2);
|
||||
default:
|
||||
return ImmutablePair.of(ALOAD, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractInsnNode getIntInsn(int num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
return new InsnNode(ICONST_0);
|
||||
case 1:
|
||||
return new InsnNode(ICONST_1);
|
||||
case 2:
|
||||
return new InsnNode(ICONST_2);
|
||||
case 3:
|
||||
return new InsnNode(ICONST_3);
|
||||
case 4:
|
||||
return new InsnNode(ICONST_4);
|
||||
case 5:
|
||||
return new InsnNode(ICONST_5);
|
||||
default:
|
||||
return new IntInsnNode(BIPUSH, num);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,33 +9,44 @@ import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.CLASS_OBJECT;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.CLASS_OBJECT;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
import static com.alibaba.testable.core.util.CollectionUtil.contains;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class OmniClassHandler extends BaseClassHandler {
|
||||
|
||||
private static final String TESTABLE_NULL_TYPE = "com.alibaba.testable.core.model.TestableNull";
|
||||
private static final String NULL_TYPE = "javax.lang.model.type.NullType";
|
||||
private static final String VOID_TYPE = "java/lang/Void";
|
||||
private static final String IGNORE = "ignore";
|
||||
private static final String METHOD_START = "(";
|
||||
private static final String VOID_METHOD_END = ")V";
|
||||
private static final String VOID_METHOD = "()V";
|
||||
private static final String ENABLE_CONFIGURATION = "Lorg/springframework/context/annotation/Configuration;";
|
||||
private static final String CLASS_ABSTRACT_COLLECTION = "java/util/AbstractCollection";
|
||||
private static final String CLASS_NUMBER = "java/lang/Number";
|
||||
private static final String CLASS_HASH_SET = "java/util/HashSet";
|
||||
|
||||
private static final String[] JUNIT_TEST_ANNOTATIONS = new String[] {
|
||||
JUnit4Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_PARAMETERIZED_TEST
|
||||
};
|
||||
private static final String[] UNREACHABLE_CLASSES = new String[] {
|
||||
CLASS_ABSTRACT_COLLECTION, CLASS_NUMBER, CLASS_HASH_SET
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if (isInterface(cn) || isJunitTestClass(cn) || isUninstantiableClass(cn)) {
|
||||
if (isInterfaceOrAtom(cn) || isJunitTestClass(cn) || isUninstantiableClass(cn) || hasSpecialAnnotation(cn)) {
|
||||
return;
|
||||
}
|
||||
addConstructorWithVoidTypeParameter(cn);
|
||||
}
|
||||
|
||||
private void addConstructorWithVoidTypeParameter(ClassNode cn) {
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, null, null);
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(VOID_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
if (cn.superName.equals(CLASS_OBJECT)) {
|
||||
@@ -43,7 +54,7 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 1;
|
||||
} else {
|
||||
constructor.instructions = invokeSuperWithTestableNullParameter(cn, start, end);
|
||||
constructor.instructions = invokeSuperWithTestableVoidParameter(cn, start, end);
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 3;
|
||||
}
|
||||
@@ -51,6 +62,18 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
cn.methods.add(constructor);
|
||||
}
|
||||
|
||||
private boolean hasSpecialAnnotation(ClassNode cn) {
|
||||
if (cn.visibleAnnotations == null) {
|
||||
return false;
|
||||
}
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (an.desc.equals(ENABLE_CONFIGURATION)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isUninstantiableClass(ClassNode cn) {
|
||||
// if the class has no even default constructor, skip it
|
||||
for (MethodNode mn : cn.methods) {
|
||||
@@ -61,9 +84,9 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isInterface(ClassNode cn) {
|
||||
// is interface or the object class
|
||||
return (cn.access & ACC_INTERFACE) != 0 || cn.superName == null;
|
||||
private boolean isInterfaceOrAtom(ClassNode cn) {
|
||||
// is interface, Object class or Void class
|
||||
return (cn.access & ACC_INTERFACE) != 0 || cn.superName == null || VOID_TYPE.equals(cn.name);
|
||||
}
|
||||
|
||||
private boolean isJunitTestClass(ClassNode cn) {
|
||||
@@ -73,10 +96,8 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
continue;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
for (String annotation : JUNIT_TEST_ANNOTATIONS) {
|
||||
if (an.desc.equals(annotation)) {
|
||||
return true;
|
||||
}
|
||||
if (contains(JUNIT_TEST_ANNOTATIONS, an.desc)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,16 +114,17 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
return il;
|
||||
}
|
||||
|
||||
private InsnList invokeSuperWithTestableNullParameter(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
private InsnList invokeSuperWithTestableVoidParameter(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(start);
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new TypeInsnNode(NEW, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE)));
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE), CONSTRUCTOR,
|
||||
VOID_METHOD, false));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, false));
|
||||
if (contains(UNREACHABLE_CLASSES, cn.superName)) {
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR, VOID_METHOD, false));
|
||||
} else {
|
||||
il.add(new VarInsnNode(ALOAD, 1));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(VOID_TYPE) + VOID_METHOD_END, false));
|
||||
}
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(end);
|
||||
return il;
|
||||
@@ -111,7 +133,7 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
private List<LocalVariableNode> createLocalVariables(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
return CollectionUtil.listOf(
|
||||
new LocalVariableNode(THIS_REF, ClassUtil.toByteCodeClassName(cn.name), null, start, end, 0),
|
||||
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(NULL_TYPE), null, start, end, 1)
|
||||
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(VOID_TYPE), null, start, end, 1)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
LogUtil.diagnose("Found source class %s", cn.name);
|
||||
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
|
||||
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
|
||||
for (MethodInfo im : injectMethods) {
|
||||
@@ -57,7 +58,11 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.diagnose(" Found method %s", mn.name);
|
||||
LogUtil.verbose(" Found method %s", mn.name);
|
||||
if (mn.name.startsWith("$")) {
|
||||
// skip methods e.g. "$jacocoInit"
|
||||
return;
|
||||
}
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
if (instructions.length == 0) {
|
||||
// native method (issue-52)
|
||||
@@ -233,7 +238,12 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
String mockMethodName = newOperatorInjectMethod.getMockName();
|
||||
int invokeOpcode = newOperatorInjectMethod.isStatic() ? INVOKESTATIC : INVOKEVIRTUAL;
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start), mockMethodName);
|
||||
String log = String.format("Line %d, mock method \"%s\" used", getLineNum(instructions, start), mockMethodName);
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(5, log);
|
||||
} else {
|
||||
LogUtil.diagnose(2, log);
|
||||
}
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
if (!newOperatorInjectMethod.isStatic()) {
|
||||
@@ -264,8 +274,13 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private AbstractInsnNode[] replaceMemberCallOps(MethodNode mn, MethodInfo mockMethod, AbstractInsnNode[] instructions,
|
||||
String ownerClass, int opcode, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
String log = String.format("Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
mockMethod.getMockName());
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(5, log);
|
||||
} else {
|
||||
LogUtil.diagnose(2, log);
|
||||
}
|
||||
if (!mockMethod.isStatic()) {
|
||||
mn.instructions.insertBefore(instructions[start], new MethodInsnNode(INVOKESTATIC, mockClassName,
|
||||
GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false));
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.handler.test.*;
|
||||
import com.alibaba.testable.agent.handler.test.Framework;
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -20,14 +20,11 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
|
||||
private int testCaseCount = 0;
|
||||
private boolean shouldGenerateCleanupMethod = true;
|
||||
private final Framework framework;
|
||||
|
||||
private final Framework[] frameworkClasses = new Framework[] {
|
||||
new JUnit4Framework(),
|
||||
new JUnit5Framework(),
|
||||
new TestNgFramework(),
|
||||
new TestNgOnClassFramework(),
|
||||
new SpockFramework()
|
||||
};
|
||||
public TestClassHandler(Framework framework) {
|
||||
this.framework = framework;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bytecode of test class
|
||||
@@ -35,11 +32,7 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
Framework framework = checkFramework(cn);
|
||||
if (framework == null) {
|
||||
LogUtil.warn("Failed to detect test framework for %s", cn.name);
|
||||
return;
|
||||
}
|
||||
LogUtil.diagnose("Found test class %s", cn.name);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
handleTestableUtil(mn);
|
||||
handleTestCaseMethod(mn, framework);
|
||||
@@ -52,28 +45,6 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
LogUtil.diagnose(" Found %d test cases", testCaseCount);
|
||||
}
|
||||
|
||||
private Framework checkFramework(ClassNode cn) {
|
||||
Set<String> classAnnotationSet = new HashSet<String>();
|
||||
Set<String> methodAnnotationSet = new HashSet<String>();
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
classAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
methodAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Framework i : frameworkClasses) {
|
||||
if (i.fit(classAnnotationSet, methodAnnotationSet)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleTestCaseMethod(MethodNode mn, Framework framework) {
|
||||
TestCaseMethodType type = framework.checkMethodType(mn);
|
||||
|
||||
@@ -3,27 +3,28 @@ package com.alibaba.testable.agent.transformer;
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.DiagnoseUtil;
|
||||
import com.alibaba.testable.agent.util.MethodUtil;
|
||||
import com.alibaba.testable.agent.util.*;
|
||||
import com.alibaba.testable.core.exception.TargetNotExistException;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InnerClassNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_CLASS;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.CLASS_OBJECT;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.KOTLIN_POSTFIX_COMPANION;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
import static com.alibaba.testable.agent.util.MethodUtil.isStatic;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.MOCK_POSTFIX;
|
||||
|
||||
public class MockClassParser {
|
||||
|
||||
private static final String CLASS_OBJECT = "java/lang/Object";
|
||||
|
||||
/**
|
||||
* Get information of all mock methods
|
||||
* @param className mock class name
|
||||
@@ -36,9 +37,8 @@ public class MockClassParser {
|
||||
return new ArrayList<MethodInfo>();
|
||||
}
|
||||
for (MethodNode mn : getAllMethods(cn)) {
|
||||
checkMethodAnnotation(cn, methodInfos, mn);
|
||||
addMethodWithAnnotationCheck(methodInfos, cn, mn);
|
||||
}
|
||||
LogUtil.diagnose(" Found %d mock methods", methodInfos.size());
|
||||
return methodInfos;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class MockClassParser {
|
||||
return false;
|
||||
}
|
||||
DiagnoseUtil.setupByClass(cn);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
for (MethodNode mn : cn.name.endsWith(MOCK_POSTFIX) ? getAllMethods(cn) : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String fullClassName = toJavaStyleClassName(an.desc);
|
||||
@@ -75,66 +75,53 @@ public class MockClassParser {
|
||||
mns.addAll(getAllMethods(scn));
|
||||
}
|
||||
}
|
||||
for (InnerClassNode innerClass : cn.innerClasses) {
|
||||
if (innerClass.name.equals(cn.name + KOTLIN_POSTFIX_COMPANION)) {
|
||||
ClassNode scn = ClassUtil.getClassNode(innerClass.name);
|
||||
if (scn != null) {
|
||||
mns.addAll(getAllMethods(scn));
|
||||
}
|
||||
}
|
||||
}
|
||||
return mns;
|
||||
}
|
||||
|
||||
private void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
private void addMethodWithAnnotationCheck(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String fullClassName = toJavaStyleClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(" Mock constructor \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
|
||||
ClassUtil.toJavaStyleClassName(MethodUtil.getReturnType(mn.desc)), mn.desc));
|
||||
if (GlobalConfig.checkMockTargetExistence) {
|
||||
checkTargetConstructorExists(cn, mn);
|
||||
}
|
||||
addMockConstructor(methodInfos, cn, mn);
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) && AnnotationUtil.isValidMockMethod(mn, an)) {
|
||||
if (LogUtil.isVerboseEnabled()) {
|
||||
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
|
||||
getTargetMethodOwner(mn, an), getTargetMethodName(mn, an), getTargetMethodDesc(mn, an)));
|
||||
methodInfos.add(new MethodInfo(ClassUtil.getSourceClassName(cn.name), CONSTRUCTOR, mn.desc, cn.name,
|
||||
mn.name, mn.desc, isStatic(mn)));
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) && isValidMockMethod(mn, an)) {
|
||||
if (GlobalConfig.checkMockTargetExistence) {
|
||||
checkTargetMethodExists(cn, mn, an);
|
||||
}
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(
|
||||
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
|
||||
if (CONSTRUCTOR.equals(targetMethod)) {
|
||||
addMockConstructor(methodInfos, cn, mn);
|
||||
MethodInfo mi = getMethodInfo(cn, mn, an, targetMethod);
|
||||
if (mi != null) {
|
||||
methodInfos.add(mi);
|
||||
} else {
|
||||
MethodInfo mi = getMethodInfo(cn, mn, an, targetMethod);
|
||||
if (mi != null) {
|
||||
methodInfos.add(mi);
|
||||
}
|
||||
LogUtil.warn("Failed to parse method %s::%s", cn.name, mn.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTargetMethodOwner(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
|
||||
null, Type.class);
|
||||
return type == null ? MethodUtil.getFirstParameter(mn.desc) : type.getClassName();
|
||||
}
|
||||
|
||||
private String getTargetMethodName(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
String name = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_METHOD,
|
||||
null, String.class);
|
||||
return name == null ? mn.name : name;
|
||||
}
|
||||
|
||||
private String getTargetMethodDesc(MethodNode mn, AnnotationNode mockMethodAnnotation) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
|
||||
null, Type.class);
|
||||
return type == null ? MethodUtil.removeFirstParameter(mn.desc) : mn.desc;
|
||||
}
|
||||
|
||||
private MethodInfo getMethodInfo(ClassNode cn, MethodNode mn, AnnotationNode an, String targetMethod) {
|
||||
Type targetType = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
boolean isStatic = isStatic(mn);
|
||||
if (targetType == null) {
|
||||
// "targetClass" unset, use first parameter as target class type
|
||||
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
|
||||
if (methodDescPair == null) {
|
||||
ImmutablePair<String, String> methodDescPair = MethodUtil.splitFirstAndRestParameters(mn.desc);
|
||||
if (methodDescPair.left.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new MethodInfo(methodDescPair.left, targetMethod, methodDescPair.right, cn.name, mn.name, mn.desc,
|
||||
@@ -147,20 +134,79 @@ public class MockClassParser {
|
||||
}
|
||||
}
|
||||
|
||||
private void addMockConstructor(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, CONSTRUCTOR, mn.desc, cn.name, mn.name, mn.desc, isStatic(mn)));
|
||||
private void checkTargetMethodExists(ClassNode cn, MethodNode mn, AnnotationNode an) {
|
||||
String targetMethodName = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
|
||||
if (targetMethodName == null) {
|
||||
targetMethodName = mn.name;
|
||||
}
|
||||
String targetClassName;
|
||||
String targetMethodDesc;
|
||||
Type targetClass = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
if (targetClass != null) {
|
||||
targetClassName = targetClass.getClassName();
|
||||
targetMethodDesc = mn.desc;
|
||||
checkMethodExists(cn.name, mn.name, targetClassName, targetMethodName, targetMethodDesc);
|
||||
} else if (mn.desc.charAt(1) == TYPE_CLASS) {
|
||||
ImmutablePair<String, String> parameterPair = MethodUtil.splitFirstAndRestParameters(mn.desc);
|
||||
targetClassName = ClassUtil.toDotSeparatedName(parameterPair.left);
|
||||
targetMethodDesc = parameterPair.right;
|
||||
checkMethodExists(cn.name, mn.name, targetClassName, targetMethodName, targetMethodDesc);
|
||||
} else {
|
||||
throw new TargetNotExistException("target class not exist", cn.name, mn.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMethodExists(String mockClassName, String mockMethodName, String targetClassName,
|
||||
String targetMethodName, String targetMethodDesc) {
|
||||
ClassNode targetClassNode = ClassUtil.getClassNode(targetClassName);
|
||||
if (targetClassNode == null) {
|
||||
throw new TargetNotExistException("target class not found", mockClassName, mockMethodName);
|
||||
}
|
||||
boolean targetFound = false;
|
||||
for (MethodNode targetMethodNode : getAllMethods(targetClassNode)) {
|
||||
if (targetMethodNode.name.equals(targetMethodName)) {
|
||||
targetFound = true;
|
||||
if (targetMethodNode.desc.equals(targetMethodDesc)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new TargetNotExistException(
|
||||
targetFound ? "mock method does not match original method" : "no such method in target class",
|
||||
mockClassName, mockMethodName);
|
||||
}
|
||||
|
||||
private void checkTargetConstructorExists(ClassNode cn, MethodNode mn) {
|
||||
String returnType = MethodUtil.getReturnType(mn.desc);
|
||||
if (returnType.charAt(0) != TYPE_CLASS) {
|
||||
throw new TargetNotExistException("return type is not a class", cn.name, mn.name);
|
||||
}
|
||||
ClassNode targetClassNode = ClassUtil.getClassNode(ClassUtil.toJavaStyleClassName(returnType));
|
||||
if (targetClassNode == null) {
|
||||
throw new TargetNotExistException("target class not found", cn.name, mn.name);
|
||||
}
|
||||
for (MethodNode targetMethodNode : targetClassNode.methods) {
|
||||
if (CONSTRUCTOR.equals(targetMethodNode.name) &&
|
||||
MethodUtil.getParameters(targetMethodNode.desc).equals(MethodUtil.getParameters(mn.desc))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new TargetNotExistException("no such constructor in target class", cn.name, mn.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split desc to "first parameter" and "desc of rest parameters"
|
||||
* @param desc method desc
|
||||
* Check is MockMethod annotation is used on a valid mock method
|
||||
* @param mn mock method
|
||||
* @param an MockMethod annotation
|
||||
* @return valid or not
|
||||
*/
|
||||
private ImmutablePair<String, String> extractFirstParameter(String desc) {
|
||||
// assume first parameter is a class
|
||||
int pos = desc.indexOf(";");
|
||||
return pos < 0 ? null : ImmutablePair.of(desc.substring(2, pos), "(" + desc.substring(pos + 1));
|
||||
private boolean isValidMockMethod(MethodNode mn, AnnotationNode an) {
|
||||
Type targetClass = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
if (targetClass != null) {
|
||||
return true;
|
||||
}
|
||||
String firstParameter = MethodUtil.getFirstParameter(mn.desc);
|
||||
return !firstParameter.isEmpty() && firstParameter.charAt(0) == TYPE_CLASS;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.agent.handler.test.*;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestClassChecker {
|
||||
|
||||
private final Framework[] frameworkClasses = new Framework[] {
|
||||
new JUnit4Framework(),
|
||||
new JUnit5Framework(),
|
||||
new TestNgFramework(),
|
||||
new TestNgOnClassFramework(),
|
||||
new SpockFramework()
|
||||
};
|
||||
|
||||
public Framework checkFramework(ClassNode cn) {
|
||||
Set<String> classAnnotationSet = new HashSet<String>();
|
||||
Set<String> methodAnnotationSet = new HashSet<String>();
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
classAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
methodAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Framework i : frameworkClasses) {
|
||||
if (i.fit(classAnnotationSet, methodAnnotationSet)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.handler.MockClassHandler;
|
||||
import com.alibaba.testable.agent.handler.OmniClassHandler;
|
||||
import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.handler.*;
|
||||
import com.alibaba.testable.agent.handler.test.Framework;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.util.*;
|
||||
import com.alibaba.testable.core.exception.TargetNotExistException;
|
||||
import com.alibaba.testable.core.model.ClassType;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.util.MockAssociationUtil;
|
||||
@@ -20,11 +19,10 @@ import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.CGLIB_CLASS_PATTERN;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.KOTLIN_POSTFIX_COMPANION;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.TEST_POSTFIX;
|
||||
import static com.alibaba.testable.core.util.PathUtil.createFolder;
|
||||
import static org.objectweb.asm.Opcodes.ACC_STATIC;
|
||||
|
||||
/**
|
||||
@@ -32,61 +30,67 @@ import static org.objectweb.asm.Opcodes.ACC_STATIC;
|
||||
*/
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_VALUE = "value";
|
||||
private static final String FIELD_TREAT_AS = "treatAs";
|
||||
private static final String COMMA = ",";
|
||||
private static final String CLASS_NAME_MOCK = "Mock";
|
||||
private static final String CLASS_JUNIT_5_NESTED = "Lorg/junit/jupiter/api/Nested;";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes, should keep these lists as tiny as possible
|
||||
*/
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "sun/", "com/sun/", "org/gradle/",
|
||||
"org/springframework/boot/autoconfigure/" };
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "sun/", "com/sun/", "javax/crypto/",
|
||||
"java/util/logging/", "org/gradle/", "org/robolectric/" };
|
||||
|
||||
public MockClassParser mockClassParser = new MockClassParser();
|
||||
private final MockClassParser mockClassParser = new MockClassParser();
|
||||
private final TestClassChecker testClassChecker = new TestClassChecker();
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
|
||||
// className is in slash-separated format
|
||||
if (isSystemClass(className)) {
|
||||
// Ignore system class and reloaded class
|
||||
return null;
|
||||
}
|
||||
LogUtil.verbose("Handle class: " + className);
|
||||
byte[] bytes = new OmniClassHandler().getBytes(classFileBuffer);
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
if (cn != null) {
|
||||
return transformMock(bytes, cn);
|
||||
byte[] bytes = GlobalConfig.enhanceOmniConstructor ?
|
||||
new OmniClassHandler().getBytes(classFileBuffer) : classFileBuffer;
|
||||
bytes = GlobalConfig.enhanceFinal ? new FinalFieldClassHandler().getBytes(bytes) : bytes;
|
||||
if (GlobalConfig.enhanceMock) {
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
if (cn != null) {
|
||||
return transformMock(bytes, cn);
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private byte[] transformMock(byte[] bytes, ClassNode cn) {
|
||||
String className = cn.name;
|
||||
String className = (GlobalConfig.getMockPackageMapping() == null) ? cn.name : mapPackage(cn.name);
|
||||
try {
|
||||
if (mockClassParser.isMockClass(cn)) {
|
||||
// it's a mock class
|
||||
LogUtil.diagnose("Found mock class %s", className);
|
||||
bytes = new MockClassHandler(className).getBytes(bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
String mockClass = foundMockForTestClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a test class with testable enabled
|
||||
LogUtil.diagnose("Found test class %s", className);
|
||||
bytes = new TestClassHandler().getBytes(bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
mockClass = foundMockForSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a source class with testable enabled
|
||||
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
|
||||
LogUtil.diagnose("Found source class %s", className);
|
||||
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
}
|
||||
}
|
||||
BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
|
||||
return bytes;
|
||||
}
|
||||
String mockClass = foundMockForSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a source class with testable enabled
|
||||
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
|
||||
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
|
||||
BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
|
||||
return bytes;
|
||||
}
|
||||
Framework framework = testClassChecker.checkFramework(cn);
|
||||
if (framework != null) {
|
||||
// it's a test class
|
||||
bytes = new TestClassHandler(framework).getBytes(bytes);
|
||||
BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
|
||||
return bytes;
|
||||
} else if (cn.name.endsWith(TEST_POSTFIX)) {
|
||||
LogUtil.verbose("Failed to detect test framework for %s", cn.name);
|
||||
}
|
||||
} catch (TargetNotExistException e) {
|
||||
LogUtil.error("Invalid mock method %s::%s - %s", e.getClassName(), e.getMethodName(), e.getMessage());
|
||||
System.exit(0);
|
||||
} catch (Throwable t) {
|
||||
LogUtil.warn("Failed to transform class " + className);
|
||||
LogUtil.warn(t.toString());
|
||||
@@ -94,10 +98,21 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
} finally {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
BytecodeUtil.dumpByte(className, getDumpPathByAnnotation(cn), bytes);
|
||||
BytecodeUtil.dumpByte(cn, null, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private String mapPackage(String name) {
|
||||
String dotSeparatedName = ClassUtil.toDotSeparatedName(name);
|
||||
for (String prefix : GlobalConfig.getMockPackageMapping().keySet()) {
|
||||
if (dotSeparatedName.startsWith(prefix)) {
|
||||
return ClassUtil.toSlashSeparatedName(GlobalConfig.getMockPackageMapping().get(prefix))
|
||||
+ name.substring(prefix.length());
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private String foundMockForSourceClass(String className) {
|
||||
String mockClass = lookForMockWithAnnotationAsSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
@@ -116,7 +131,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private String foundMockForTestClass(String className) {
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
ClassNode cn = adaptInnerClass(ClassUtil.getClassNode(className));
|
||||
if (cn != null) {
|
||||
String mockClass = lookForMockWithAnnotationAsTestClass(cn);
|
||||
if (mockClass != null) {
|
||||
@@ -130,6 +145,18 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return lookForOuterMockClass(className);
|
||||
}
|
||||
|
||||
private ClassNode adaptInnerClass(ClassNode cn) {
|
||||
if (cn == null || cn.visibleAnnotations == null) {
|
||||
return cn;
|
||||
}
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (an.desc.equals(CLASS_JUNIT_5_NESTED)) {
|
||||
return ClassUtil.getClassNode(ClassUtil.toOuterClassName(cn.name));
|
||||
}
|
||||
}
|
||||
return cn;
|
||||
}
|
||||
|
||||
private String lookForOuterMockClass(String className) {
|
||||
String mockClassName = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
|
||||
if (mockClassParser.isMockClass(ClassUtil.getClassNode(mockClassName))) {
|
||||
@@ -143,20 +170,22 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
if (null == className || className.contains(CGLIB_CLASS_PATTERN)) {
|
||||
return true;
|
||||
}
|
||||
String whitePrefix = GlobalConfig.getPkgPrefix();
|
||||
if (whitePrefix != null) {
|
||||
for (String prefix : whitePrefix.split(COMMA)) {
|
||||
if (className.startsWith(prefix)) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return false;
|
||||
}
|
||||
}
|
||||
String[] blackList = GlobalConfig.getPkgPrefixBlackList();
|
||||
if (blackList != null && isInPrefixList(className, blackList)) {
|
||||
return true;
|
||||
} else {
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String[] whiteList = GlobalConfig.getPkgPrefixWhiteList();
|
||||
if (whiteList != null) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return !isInPrefixList(className, whiteList);
|
||||
}
|
||||
return isInPrefixList(className, BLACKLIST_PREFIXES);
|
||||
}
|
||||
|
||||
private boolean isInPrefixList(String name, String[] prefixList) {
|
||||
for (String prefix : prefixList) {
|
||||
if (name.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -221,11 +250,11 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
private String parseMockWithAnnotation(ClassNode cn, ClassType expectedType) {
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toJavaStyleClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
|
||||
if ((ClassUtil.toByteCodeClassName(ConstPool.MOCK_WITH)).equals(an.desc)) {
|
||||
ClassType type = AnnotationUtil.getAnnotationParameter(an, FIELD_TREAT_AS, ClassType.GuessByName,
|
||||
ClassType.class);
|
||||
if (isExpectedType(cn.name, type, expectedType)) {
|
||||
Type clazz = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE,
|
||||
Type clazz = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_VALUE,
|
||||
Type.getType(NullType.class), Type.class);
|
||||
DiagnoseUtil.setupByClass(ClassUtil.getClassNode(clazz.getClassName()));
|
||||
return clazz.getClassName();
|
||||
@@ -236,21 +265,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDumpPathByAnnotation(ClassNode cn) {
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toJavaStyleClassName(an.desc).equals(ConstPool.DUMP_TO)) {
|
||||
String path = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE, null, String.class);
|
||||
String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path);
|
||||
if (createFolder(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isExpectedType(String className, ClassType type, ClassType expectedType) {
|
||||
if (type.equals(ClassType.GuessByName)) {
|
||||
return expectedType.equals(ClassType.TestClass) == className.endsWith(TEST_POSTFIX);
|
||||
@@ -260,7 +274,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private String getInnerMockClassName(String className) {
|
||||
return className + DOLLAR + CLASS_NAME_MOCK;
|
||||
return className + DOLLAR + GlobalConfig.innerMockClassName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_CLASS;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -65,15 +60,4 @@ public class AnnotationUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is MockMethod annotation is used on a valid mock method
|
||||
* @param mn mock method
|
||||
* @param an MockMethod annotation
|
||||
* @return valid or not
|
||||
*/
|
||||
public static boolean isValidMockMethod(MethodNode mn, AnnotationNode an) {
|
||||
Type targetClass = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
String firstParameter = MethodUtil.getFirstParameter(mn.desc);
|
||||
return targetClass != null || firstParameter.charAt(0) == TYPE_CLASS;
|
||||
}
|
||||
}
|
||||
|
||||