Compare commits

..

79 Commits

Author SHA1 Message Date
金戟
09ecd0d7c0 release v0.6.8 2021-08-08 21:53:34 +08:00
金戟
93db467f43 update maintain plan 2021-08-08 21:33:58 +08:00
金戟
a78931ae81 allow modify static final field 2021-07-14 21:29:32 +08:00
金戟
93ad1fa94f allow to dump single mock class 2021-07-12 21:41:31 +08:00
金戟
2780ccc6fa for better performance 2021-07-12 20:37:13 +08:00
金戟
fd3b088f59 release v0.6.7 2021-06-11 17:36:43 +08:00
金戟
e812aa6126 fix stack effect value of xaload opcode 2021-06-11 17:25:38 +08:00
金戟
b949f57d54 in favor of static import 2021-06-06 23:04:42 +08:00
金戟
0551240c7d fix incorrect class name in mock target checking log 2021-06-01 19:40:22 +08:00
金戟
8a3a0cf6bf add with thread pool doc 2021-06-01 10:58:00 +08:00
金戟
d3ab246c78 should travel parent classes 2021-05-31 23:36:53 +08:00
金戟
142b0e5a1e update doc for v0.6.6 2021-05-27 19:03:11 +08:00
金戟
38221a67ef release 0.6.6 2021-05-22 19:00:35 +08:00
金戟
993c178adf fit mock target checking for kotlin 2021-05-22 18:31:03 +08:00
金戟
3786c6251e always transform all test classes 2021-05-22 18:12:26 +08:00
金戟
e5f8e5244c construct void object is unnecessary 2021-05-22 16:36:06 +08:00
金戟
e13dce7ac6 prefer to use minimal exception count 2021-05-22 11:12:58 +08:00
金戟
3265803dec expose constructor of void class 2021-05-22 01:27:12 +08:00
金戟
ef3891bd57 add release note 2021-05-22 01:24:40 +08:00
金戟
fcbdb7275b add test case of basic type constructing 2021-05-21 00:25:02 +08:00
金戟
ea4c6c5b3e allow disable mock target checking 2021-05-16 23:16:51 +08:00
金戟
381ff52515 also check methods from parent classes 2021-05-15 16:16:01 +08:00
金戟
c5f39dfae3 refactor logs, print mock method information in mock class handler 2021-05-15 15:57:02 +08:00
金戟
7f8ef70e57 move byte code logic to util class 2021-05-15 15:20:16 +08:00
金戟
23af64475a support target exist checking of mock method 2021-05-15 11:04:45 +08:00
金戟
7181b49211 package prefix should end with a slash 2021-05-14 21:24:34 +08:00
金戟
cd7b9cc922 allow disable mocking 2021-05-11 22:42:31 +08:00
金戟
46288fa0e9 add detail release plan 2021-05-05 23:23:11 +08:00
金戟
e61597ba8b add package mapping doc 2021-05-04 13:22:07 +08:00
金戟
c577301493 support mock package mapping 2021-05-03 14:56:33 +08:00
金戟
a33908db41 support absolute config path and update docs 2021-05-03 11:39:21 +08:00
金戟
4ea97fe13f always print out context path of testable agent 2021-05-03 10:56:36 +08:00
金戟
7cda8ffd36 add plan for mybatis mock function 2021-05-03 10:37:33 +08:00
金戟
1da98a0364 small fix for android demo 2021-05-03 09:30:23 +08:00
金戟
c6d892da2d global exclude robolectric package from omni processor 2021-05-02 20:11:55 +08:00
金戟
d1f2c3da97 add a mini android demo 2021-05-02 20:10:07 +08:00
金戟
f40cfe929a always static import asserts 2021-05-02 09:25:36 +08:00
金戟
96d1f9d633 release v0.6.5 2021-05-02 08:10:58 +08:00
金戟
350802fc9b fix mock method in super class ignored in association scope 2021-05-01 19:28:26 +08:00
金戟
0ae5163435 revert test code 2021-05-01 17:31:13 +08:00
金戟
0e581a7eeb add kotlin demo for mock scope 2021-05-01 17:25:48 +08:00
金戟
b68ff99800 rename construction options 2021-05-01 16:58:02 +08:00
金戟
7754f828cc fix issue of invoking method with single array parameter 2021-05-01 16:05:42 +08:00
金戟
3d62c91291 association check require at least 6 stack space 2021-04-29 13:23:19 +08:00
金戟
14c3d841c5 add demo for test scope 2021-04-28 23:52:52 +08:00
金戟
5f6cf3888c static final field is not assignable during runtime 2021-04-26 08:30:57 +08:00
金戟
75c6a67b8f support construction options 2021-04-25 23:27:51 +08:00
金戟
d11402dd6c update description of pkgPrefix 2021-04-25 18:25:20 +08:00
金戟
db771b9c66 fix an omni constructor issue caused by array-in-array type 2021-04-25 18:24:13 +08:00
金戟
338c7828c8 skip jacoco generated members and do not travel array of basic classes 2021-04-24 23:30:48 +08:00
金戟
8ebe2a5d0c allow constructor with parameter of same type as class under constructing 2021-04-24 18:46:04 +08:00
金戟
4f3e3f0093 throw with information about the class failed to construct 2021-04-24 18:14:53 +08:00
金戟
9924d7d2fc release v0.6.4 2021-04-24 17:43:43 +08:00
金戟
9f5867ed76 support nested annotation of junit 5 2021-04-24 16:59:29 +08:00
金戟
be0a580a04 AbstractCollection is unranchable in java agent 2021-04-24 16:23:01 +08:00
金戟
78d7ee21a6 use void type instead of null type 2021-04-24 11:47:41 +08:00
金戟
af409a65a8 fix circle check rounded by getType not equals with getClass 2021-04-23 21:41:30 +08:00
金戟
a67619d574 add verbose log for omni constructor 2021-04-23 20:34:15 +08:00
金戟
b89daa22c3 always skip null element when traveling class 2021-04-23 19:30:54 +08:00
金戟
e948e625ea better to skip classes in javax/crypto package 2021-04-21 16:32:00 +08:00
金戟
d463f863db local variable table of non-static method can be empty 2021-04-20 21:17:09 +08:00
金戟
31140bef87 add description of global properties configurations 2021-04-18 15:06:36 +08:00
金戟
d2944a88fb reorder parameters 2021-04-18 13:37:36 +08:00
金戟
9d6e4dd1fa bump to v0.6.3 2021-04-18 10:26:06 +08:00
金戟
9c09bf9653 avoid to select constructor with parameter of its own type 2021-04-18 09:45:57 +08:00
金戟
6d87653fa2 handle non-static method without this reference 2021-04-18 09:15:03 +08:00
金戟
eec88f4bd9 should convert package prefix to slash separated format 2021-04-17 19:52:32 +08:00
金戟
5921ccab07 allow skip specified class from omni enhancer 2021-04-17 19:24:02 +08:00
金戟
8dde402a8c get resource could be null 2021-04-17 18:45:29 +08:00
金戟
d10478f95f package prefix should auto append dot 2021-04-17 17:46:35 +08:00
金戟
0c063e9b1f support custom inner mock class name 2021-04-17 17:16:02 +08:00
金戟
b8771f0dac simplify premain logic 2021-04-16 12:52:24 +08:00
金戟
42dd58584b mark date as basic type 2021-04-14 09:28:07 +08:00
金戟
ce55bb4c9a skip all configuration class for omni handler 2021-04-12 16:40:53 +08:00
金戟
cff2d1ae5a fix release script 2021-04-09 14:25:02 +08:00
金戟
5904ad99cb release v0.6.2 2021-04-07 21:38:24 +08:00
金戟
74fbc5b53f support configure file 2021-04-07 09:40:20 +08:00
金戟
f6b5bab250 manual generated serialVersionUID not works 2021-04-07 00:05:29 +08:00
金戟
f2feb3cc68 more precise omni handler filter 2021-04-06 23:08:30 +08:00
134 changed files with 2881 additions and 805 deletions

View File

@@ -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)提交您的代码,我们将尽快回复并处理。
## 目录结构

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
/build

View 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'
}

View 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

View File

@@ -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)
}
}

View 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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.alibaba.testable.demo.model;
public interface Color {
String getColor();
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Testable Android Demo</string>
</resources>

View 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>

View File

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

View File

@@ -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.");
}
}

View File

@@ -0,0 +1 @@
log.file = null

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

View 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

View 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
View 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
View 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

View File

@@ -0,0 +1,2 @@
include ':app'
rootProject.name = "Testable Android Demo"

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
omni.constructor.enhance.enable = true
mock.target.checking.enable = true

View File

@@ -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> {

View File

@@ -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>

View File

@@ -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"
}
}
}

View File

@@ -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()
}
}

View File

@@ -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"
}
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -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())
}
}

View File

@@ -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>) {
}
}

View File

@@ -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"))
}
}

View File

@@ -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)
}

View File

@@ -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())
}
}

View File

@@ -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)

View File

@@ -0,0 +1,2 @@
omni.constructor.enhance.enable = true
mock.target.checking.enable = true

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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?

View File

@@ -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 .

View File

@@ -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 issuesthanks 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

View File

@@ -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.

View File

@@ -0,0 +1,4 @@
Use Package Mapping
---
TO BE TRANSLATED

View File

@@ -0,0 +1,4 @@
Mock Invocation In Thread Pool
---
TO BE TRANSLATED

View File

@@ -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)

View File

@@ -58,3 +58,12 @@
| --- | --- | --- | ---- | --- |
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
#### @DumpTo
将指定类型被`TestableMock`处理过后的字节码导出到文件。
- 作用于:任意类型
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| N/A | String | 是 | N/A | 存放导出文件的目录(相对当前执行目录的相对路径),若目录不存在,将自动创建 |

View File

@@ -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有关请尽量提供可复现的相关源码或示例以便进行针对性定位和排查。

View File

@@ -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 ..."错误?

View File

@@ -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&amp;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`文件中,连接参数的`&`符号需要写为`&amp;`
对于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)。

View File

@@ -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 {

View File

@@ -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方法的返回值使用完全初始化的对象能够更好保障测试的可靠性。

View File

@@ -42,6 +42,8 @@
### 2.1 直接访问私有成员
> 由于IDE语法报错原因此特性计划在未来版本中移除建议采用`PrivateAccessor`方式
第二种方法,除了借助`PrivateAccessor`工具类以外,凡是使用了`@EnablePrivateAccess`注解的测试类还会被自动赋予以下“特殊能力”:
- 调用**被测类**的私有方法(包括静态方法)

View File

@@ -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和访问被测类私有成员

View File

@@ -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);
> }
> ```
> ```

View File

@@ -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`示例项目。

View File

@@ -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方法。

View 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容器类并根据情况决定是否需要进行包路径映射

View 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}"
... // 其他测试配置
}
```

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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));

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More