Compare commits

...

97 Commits
0.6 ... v0.6.7

Author SHA1 Message Date
金戟
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
金戟
b41923bfa0 release v0.6.1 2021-04-06 08:09:23 +08:00
金戟
7c1f18de46 optimize docs 2021-04-06 00:03:38 +08:00
金戟
c393d40d0d better log description, handling -> found 2021-04-05 15:52:12 +08:00
金戟
d4a69528a7 use relative path for bytecode dumping 2021-04-05 14:58:29 +08:00
金戟
a46e73a421 support specify log file path 2021-04-05 14:09:44 +08:00
金戟
01e9890234 always generate log file 2021-04-05 11:56:06 +08:00
金戟
93cdef1d6c remove gradle settings in kotlin demo 2021-04-04 15:38:43 +08:00
金戟
e2e986e50d add a tiny demo for spock 2021-04-04 15:31:18 +08:00
金戟
f5475aa18e simplify log level config 2021-04-04 00:10:56 +08:00
金戟
844323af3a fix compatibility with spring boot 2021-04-03 23:54:01 +08:00
金戟
cc2deeff1c ClassCircularityError is not an exception 2021-04-03 23:35:22 +08:00
金戟
14e4d2db5b ignore cglib classes 2021-04-03 22:51:28 +08:00
金戟
c9c8ba6c24 junit test class can also has parent 2021-04-03 22:10:18 +08:00
金戟
b5c0afb3da ignore any exception when try load class 2021-04-03 18:17:11 +08:00
金戟
617d372545 avoid to transfer gradle TestWorker class 2021-04-03 18:16:36 +08:00
金戟
58da25de53 add mock verifier support spock 2021-04-03 13:41:51 +08:00
金戟
46706ba56c add supported test framwork 2021-04-03 10:02:03 +08:00
金戟
1ced04bf29 fix loging 2021-04-02 23:50:39 +08:00
金戟
7b2f967626 add tool scripts 2021-04-01 09:22:28 +08:00
金戟
e0a4a39d55 move dump method to util package 2021-03-31 10:34:45 +08:00
金戟
b4da23a90e adopt for groovy bytecode 2021-03-31 10:34:19 +08:00
金戟
c1659242b6 fix markdown format 2021-03-28 15:10:52 +08:00
金戟
929abffb66 add english doc for omni methods 2021-03-28 14:47:00 +08:00
161 changed files with 3897 additions and 1102 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# maven ignore
target/
.mvn/
# gradle ignore
build/

View File

@@ -11,7 +11,7 @@
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
-----
@@ -19,8 +19,12 @@
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.7` 完善Mock功能的防代码重构机制支持快速Mock指定类型的所有方法
- `0.6` 当前版本,正在开发中的功能包括:
- 支持Mock Lambada语句中的方法引用 [#36](https://github.com/alibaba/testable-mock/issues/36)
- 其他进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.7` 主要计划包括:
- 增加DAO层逻辑的单元测试辅助 [介绍](https://alibaba.github.io/testable-mock/#/zh-cn/doc/verify-sql)
- 支持快速Mock指定类型的所有方法 [#82](https://github.com/alibaba/testable-mock/issues/82)
- `1.0` 功能稳定,一个崭新的开始
## 目录结构
@@ -32,6 +36,7 @@
|-- testable-agent ➜ JavaAgent模块提供Mock测试相关功能
|-- testable-core ➜ 基础功能模块提供Mock相关注解和工具类
|-- testable-maven-plugin ➜ Maven插件模块用于简化JavaAgent注入
|-- tool ➜ 项目开发过程中的工具脚本
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码

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
@@ -24,6 +24,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|-- testable-agent ➜ JavaAgent module, provides test mocking related functions
|-- testable-core ➜ Basic function module, provides mock related class and annotation
|-- testable-maven-plugin ➜ Maven plugin module, for simplify JavaAgent injection
|-- tool ➜ Scripts for project maintain
|-- demo
| |-- java-demo ➜ Java code example
| `-- kotlin-demo ➜ Kotlin code example

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.7'
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.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
testImplementation('com.alibaba.testable:testable-all:0.6.7')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.7')
}
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.0</testable.version>
<testable.version>0.6.7</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.0")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.0")
testImplementation("com.alibaba.testable:testable-all:0.6.7")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.7")
}
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.0</testable.version>
<testable.version>0.6.7</testable.version>
</properties>
<dependencies>

View File

@@ -1 +0,0 @@
rootProject.name = "demo"

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

@@ -3,7 +3,7 @@ package com.alibaba.demo.java2kotlin
import java.io.File
import java.io.IOException
object PathUtil {
object PathDemo {
fun deleteRecursively(file: File) {
if (!file.exists()) {

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

@@ -5,7 +5,7 @@ import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import org.junit.jupiter.api.Test
import java.io.File
class PathUtilTest {
class PathDemoTest {
class Mock {
@MockMethod
@@ -42,7 +42,7 @@ class PathUtilTest {
@Test
fun should_mock_java_method_invoke_in_kotlin() {
PathUtil.deleteRecursively(File("/a/b/"))
PathDemo.deleteRecursively(File("/a/b/"))
verify("listFiles").withTimes(2)
verify("delete").withTimes(4)
}

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

@@ -12,6 +12,7 @@
<modules>
<module>java-demo</module>
<module>kotlin-demo</module>
<module>spock-demo</module>
</modules>
</project>

5
demo/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
rootProject.name = 'demos'
include 'java-demo'
include 'kotlin-demo'
include 'spock-demo'

View File

@@ -0,0 +1,28 @@
plugins {
id 'groovy'
}
group = 'com.alibaba.testable'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '8'
repositories {
mavenLocal()
mavenCentral()
}
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.7')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.7')
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
useJUnitPlatform()
}

86
demo/spock-demo/pom.xml Normal file
View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Refer to: https://github.com/spockframework/spock-example/blob/master/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>spock-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>spock-demo</name>
<description>Spock demo for Testable</description>
<properties>
<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.7</testable.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-bom</artifactId>
<version>2.0-M5-groovy-3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-junit4</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- The gmavenplus plugin is used to compile Groovy code -->
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
<useFile>false</useFile>
<includes>
<include>**/*Test.java</include>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.github.pbetkier.spockdemo;
import com.github.pbetkier.spockdemo.model.SpockBox;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
public class DemoSpock {
public SpockBox createBoxOfNum() {
SpockBox box = new SpockBox();
for (int i = 1; i <= 3; i++) {
box.put(String.valueOf(i));
}
return box;
}
}

View File

@@ -0,0 +1,27 @@
package com.github.pbetkier.spockdemo.model;
import java.util.ArrayList;
import java.util.List;
public class SpockBox {
private List<String> contents = new ArrayList<>();
public int size() {
return contents.size();
}
public void put(String data) {
contents.add(data);
}
public String pop() {
if (contents.isEmpty()) {
return null;
}
String data = contents.get(size() - 1);
contents.remove(size() - 1);
return data;
}
}

View File

@@ -0,0 +1,44 @@
package com.github.pbetkier.spockdemo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import com.github.pbetkier.spockdemo.model.SpockBox
import spock.lang.Shared
import spock.lang.Specification
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
class DemoSpockTest extends Specification {
@Shared
def demoSpock = new DemoSpock()
static class Mock {
@MockConstructor
SpockBox createBox() {
SpockBox box = new SpockBox()
box.put("mock zero")
return box
}
@MockMethod(targetMethod = "put")
void putBox(SpockBox self, String data) {
self.put("mock " + data)
}
}
def "should get a box of numbers"() {
given:
def box = demoSpock.createBoxOfNum()
expect:
box.size() == 4
box.pop() == "mock 3"
box.pop() == "mock 2"
box.pop() == "mock 1"
box.pop() == "mock zero"
verify("createBox").withTimes(1)
verify("putBox").withInOrder("1").withInOrder("2").withInOrder("3")
}
}

View File

@@ -1,4 +1,29 @@
How Mock Works
---
TO BE TRANSLATED
This document mainly introduces the design and implementation principles of the mock function in `TestableMock`.
Unlike common mock-tools which write mock definitions in each test case, `TestableMock` allows each business class to provide its own set of mock methods, describing what should be mocked during testing, and define the corresponding replacement logic (that is, each business class has its own independent "Test class" and independent "Mock class"). By adopting "convention is better than configuration" principle, `TestableMock` not only reduce redundant codes but also reduce the cost of mock learning.
This design is based on two basic assumptions:
1. In any business class, methods that need to be mocked in one test case are usually required to be mocked in other test cases. For these mocked methods usually requiring external dependencies that are not easy to test.
2. Each unit test only focuses on the logic inside the unit under test, and irrelevant calls outside the unit should be replaced with mocks. That is, the invocations that need to be mocked should all be in the code of the class under test.
Accordingly, the unit test scenarios that meet the above assumptions are simplified through conventions, and the remaining more complex use scenarios are supported through configuration.
The mechanism of `TestableMock` can be summarized in one sentence: <u>Using java agent to dynamically modify the bytecode, before the unit test is about to run, replace all invocations in the business class under test which match the mock method definition with invocations to the mock method itself.</u>.
The final effect is that no matter what service framework or object-container the code uses, no matter whether the object of mock target is injected by a framework, created by new operation, and whether the target method of mock is private or external, defined as global, local, static, inherited or overloaded, all can be mocked in a same and simple way, which make unit testing much easier.
> Notice: Mock's goal is the method invocation in the class under test. The code inside the test case will not be mocked, and the method definition itself has not changed, but the invocation code to those methods will be replaced.
Specifically, when the unit test is started, `TestableMock` will preprocess the classes loaded into memory and establish the association relationship between the "class under test", the "test class", and the "mock container class" respectively (can be one-to-one or many-to-one). On the one hand, this association is to correctly match the mock call and replace it before the test case is executed, and on the other hand, it is used to control the effective scope of the mock method.
For the class under test, replace the matched call with a call to the mock container method.
For the test class, insert the mock context initialization code at the beginning of each test case.
For the mock container class, add the `testableIns()` method to make the class become a singleton class, and insert codes to record the call at the beginning of each Mock method.
The above is the core logic of the entire mocking logic. For more implementation details, please refer to the source code. If you have any questions, suggestions, or improvement proposals, you are welcome to participate in the discussion and contribute through Github Issue and Pull Request 😃

View File

@@ -11,7 +11,15 @@ Create the class under test object directly, and then use the ability of `Testab
For mock tools that modify the class loader or the bytecode of the class under test, such as `PowerMock` and `JMockit`, there is no yet case to prove that they will conflict with `TestableMock`, but in principle, there may be a risk of incompatibility between the two, Please use with caution.
#### 3. How to implement the mock method when the parent class variable points to the child class object?
#### 3. Which test frameworks does `TestableMock` support?
The `PrivateAccessor` / `OmniConstructor` / `OmniAccessor` and basic functionality of `TestableMock` are theoretically test-frameworks-independent, and should work with any test frameworks out-of-box.
The only test-framework-related part is the mock invocation verifier, which currently supports `JUnit 4` / `JUnit 5` / `TestNG` and `Spock` appropriately.
If you encounter compatibility problem under a specific test framework, or want to add mock invocation verifier support for any other frameworks, please raise an [issue](https://github.com/alibaba/testable-mock/issues) to tell us.
#### 4. How to implement the mock method when the parent class variable points to the child class object?
In the code, there are often cases of using <u>interface variables or parent class variables</u> to point to an instance of subclasses and calling methods provided by the parent class or subclass.
@@ -21,7 +29,7 @@ Therefore, regardless of whether the actually called method comes from the paren
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
#### 4. How to mock generic methods (template methods)?
#### 5. How to mock generic methods (template methods)?
Just use the same generic parameters directly on the mock method.
@@ -29,25 +37,27 @@ See the use case of the `DemoTemplateTest` test class in the Java and Kotlin exa
> Because JVM has a generic erasure mechanism, you can also directly use the `Object` type to replace generic parameters for Java projects, see the commented out "Second solution" example in the Java version of the `DemoTemplateTest` test class.
#### 5. How to mock invocation inside an inner class?
#### 6. How to mock invocation inside an inner class?
Just put mock methods in the mock container class of its outer class, it works for code in all inner classes.
See the use case of the `DemoInnerClass` test class in the Java and Kotlin examples.
#### 6. Why mocking methods in the `String` class in the Kotlin project does not work?
#### 7. Why mocking methods in the `String` class in the Kotlin project does not work?
The `String` type in Kotlin language is actually `kotlin.String` instead of `java.lang.String`. However, when this type is built from bytecode, it will be replaced with Java's `java.lang.String` class, so no matter if the mock target is written as `kotlin.String` or `java.lang.String`, it cannot match the original called method.
In actual scenarios, there are very few scenarios where methods in the `String` class need to be mocked, so `TestableMock` has not dealt with this situation specifically.
#### 7. Can `TestableMock` be used for testing Android projects?
#### 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.
#### 8. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
This problem is caused by the system `Class Path` content is too long, and has nothing to do with `TestableMock`. However, it should be noted that IntelliJ provides two auxiliary solutions: `JAR manifest` and `classpath file`. If `TestableMock` is used in the test, please select `JAR manifest`.

View File

@@ -1,6 +1,117 @@
Parameter constructor
Parameter Constructor
---
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
In unit testing, the preparation and construction of test data is a necessary and tedious task. Object-oriented layer-by-layer encapsulation becomes an obstacle to initializing the state of the object during testing. Especially when the type structure is complicated, there is no suitable construction method, or some fields need to use private inner classes, etc. Using conventional methods to construct those class often appear to be inadequate.
This feature is planned to be released in the `0.6` version.
For this reason, `TestableMock` provides two minimalist tool classes, `OmniConstructor` and `OmniAccessor`, which makes the construction of any object no longer difficult.
### 1. Construct any object with one line of code
No matter how special the target type is, `OmniConstructor` will hand it to you immediately~~ The universal object constructor `OmniConstructor` has two static methods:
- `newInstance(<AnyClass>)` ➜ Specify any type, and return an object of that type
- `newArray(<AnyClass>, <ArraySize>)` ➜ Specify any type, and return an array of that type
Usage example:
```java
// Construct a object of "WhatEverClass" type
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
// Construct a array of "WhatEverClass[]" type with capability of 5
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
```
Beside that, object constructed by `OmniConstructor` is not just a simple empty object with all member values of `null`, but a "fullness" object in which all members and all sub-members of all members have been recursively initialized. Compared with using `new` operation, `OmniConstructor` can ensure the integrity of the object structure and avoid the `NullPointerException` problem caused by partial initialization of test data.
```java
// Construct object using new operation
Parent parent = new Parent();
// Inner member is not initialized, will cause NullPointerException (❌)
parent.getChild().getGrandChild();
// Construct object using OmniConstructor
Parent parent = OmniConstructor.newInstance(Parent.class);
// No need to worry, visit any child member safely (✅)
parent.getChild().getGrandChild().getContent();
```
> **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 .
In the `DemoOmniMethodsTest` test class of the `java-demo` and `kotlin-demo` sample projects, it is shown in detail how `OmniConstructor` could be used when the target type has a multi-layered nested structure, the construction method is throwing exception, and even without public construction method available.
### 2. Access any inner member with one line of code
For test data, even with complex structure, it is usually only part of its attributes and states that are related to a specific test case. However, it is sometimes not easy to assign values to these fields deep wrapped in the object structure.
As an enhanced version of the `PrivateAccessor` tool, `OmniAccessor` is inspired by the [XPath node selector](https://www.w3schools.com/xml/xpath_syntax.asp) in the `XML` language, It provides two main static methods of `get` and `set`:
- `get(arbitrary object, "access path")` ➜ returns all member objects searched for based on path-matching
- `set(arbitrary object, "access path", new value)` ➜ Assign a value to any objects based on path-matching
There is also a `getFirst()` method used to directly obtain the unique target object during exact path matching. Its function is equivalent to `OmniAccessor.get(...).get(0)`:
- `getFirst(arbitrary object, "access path")` ➜ returns the first member object searched based on path-matching
You only need to write the access path that meets the rules, no matter what type and depth of members, you can directly reach them with one line of code:
```java
// Get all field of the parent object, which named as content and inside type GrandChild
OmniAccessor.get(parent, "{GrandChild}/content");
// Assign 100 to any fields which named as value and inside any child member that matches the 3rd item of the array named children
OmniAccessor.set(parent, "children[2]/*/value", 100);
```
The path rules are as follows:
**1. Matching member name**
The path name without additional decoration will match any member object with the same name
- `child`: match any descendant member whose name is `child`
- `child/grandChild`: matches the child member named `grandChild` among the descendants of the name `child`
**2. Matching member type**
Use curly braces to match the type name, usually used to obtain or assign multiple member objects of the same type in batches
- `{Child}`: match all descendants of `Child`
- `{Children[]}`: match all descendants of the `Children` array
- `{Child}/{GrandChild}`: match all descendant members of `Child`, all types are children of `GrandChild`
The member name and type can be mixed on the path (currently it is not supported to specify the member name and type at the same time in the same level path)
- `child/{GrandChild}`: match all descendant members whose name is `child`, all types are child members of `GrandChild`
- `{Child}/grandChild/content`: match all descendant members whose type is `Child`, the child members named `grandChild`, and the child members named `content`
**3. Use subscripts to access array members**
Use square brackets with numerical values to indicate that the matching position is an array type, and the object with the specified subscript is taken (without subscript, when the matching object is an array type, all objects in the array are matched)
- `children[1]/content`: match the descendant members of the array type named `children`, and take the child member named `content` in the `2`th object
- `parent/children[1]`: match the child member of the array type named `children` among the descendant members named `parent`, and take the `2`th object among them
**4. Use wildcards**
Wildcards can be used to match member names or type names
- `child*`: match all descendant members whose name starts with `child`
- `{*Child}`: match all descendant members whose type ends with `Child`
- `c*ld/{Grand*ld}`: match the descendant members whose name starts with `c` and ends with `ld`, and the members whose type starts with `Grand` and ends with `ld`
- `child/*/content`: At this time, `*` will match any member, that is, the child member of `content` contained in any child member of the `child` object
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoOmniMethodsTest`.
### 3. Special instructions
> **Do you really need to use `OmniAccessor`? **
>
> `OmniAccessor` implement the basic anti-refactoring mechanism based on the Fail-Fast principle. When the access path provided by the user cannot match any member, the `OmniAccessor` will immediately throw a `NoSuchMemberError` error, so that the unit test is terminated early. However, compared to the conventional member access method, the support of `OmniAccessor` in IDE refactoring is still weak.
>
> For content assignment of complex objects, in most cases, we recommend using [Builder Pattern](https://www.geeksforgeeks.org/builder-pattern-in-java/), or exposing Getter/Setter method implementations. Although these conventional methods are slightly clumsy (especially when you need to assign values to many similar members in batches), they are more friendly to the encapsulation and reconstruction of business logic.
> Only when the original type is not suitable for transformation, and there is no other way to access the target member, `OmniAccessor` is the last resort.

View File

@@ -1,5 +1,43 @@
# Release Note
## 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 framework compatibility issue
- fix a `Gradle` unit test runtime compatibility issue
## 0.6.0
- add `OmniConstructor` and `OmniAccessor` tool for parameter preparation
- fix a `ClassFormatError` caused by incorrect `FRAME FULL` operation

View File

@@ -1,12 +1,13 @@
Use TestableMock
---
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
`TestableMock` is now not only a lightweight and easy-to-use unit testing mock tool, but also a comprehensive set of auxiliary tools aimed at **simplifying Java unit testing**, including the following functions:
- [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
- [Assist test SQL generated by mybatis](en-us/doc/verify-sql.md): provide built-in mock implementation for common sql libraries, solve the problem that the logic in data-access-object (DAO) layer cannot be tested directly
## Use in Maven project
@@ -16,7 +17,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
```xml
<properties>
<testable.version>0.6.0</testable.version>
<testable.version>0.6.7</testable.version>
</properties>
```
@@ -63,8 +64,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.6.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
testImplementation('com.alibaba.testable:testable-all:0.6.7')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.7')
}
```
@@ -78,7 +79,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,4 +93,22 @@ 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>
> ```
>
> For Gradle project, add a `sourceCompatibility` property inside the `build.gradle` file, e.g.
> ```groovy
> sourceCompatibility = '6' // or 7/8/9/...
> ```
>
> See project `demo/spock-demo` for a complete example.

View File

@@ -3,52 +3,45 @@ Self-Help Troubleshooting
Compared with `Mockito` and other mock tools where developers have to manually inject mock classes, `TestableMock` uses method name and parameter type matching to automatically find invocations that require mock. While this mechanism brings convenience, it may also cause unexpected mock replacement.
To troubleshoot mock-related issues, just add the `@MockDiagnose` annotation to the mock class, and set the value `LogLevel.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
For this reason, `TestableMock` will automatically save the mock scanning log of the last test run in the project build directory. The default location is `target/testable-agent.log` (Maven project) or `build/testable-agent.log` (Gradle project).
```java
class DemoTest {
@MockDiagnose(LogLevel.ENABLE)
public static class Mock {
...
}
}
```
The output log example is as follows:
Examples of log content are as follows:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 27, mock method "innerFunc" used
[DIAGNOSE] Line 27, mock method "staticFunc" used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 34, mock method "trim" used
[DIAGNOSE] Line 34, mock method "sub" used
[DIAGNOSE] Line 34, mock method "startsWith" used
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
... ...
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[INFO] Found method newFunc
[INFO] Line 19, mock method "createBlackBox" used
[INFO] Found method outerFunc
[INFO] Line 27, mock method "innerFunc" used
[INFO] Line 27, mock method "staticFunc" used
[INFO] Found method commonFunc
[INFO] Line 34, mock method "trim" used
[INFO] Line 34, mock method "sub" used
[INFO] Line 34, mock method "startsWith" used
... ...
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
```
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
- Self troubleshooting:
According to the targeted test classes, below are some simple clues for self-troubleshooting. Suppose the class under test is "com.demo.BizService", the test class is "com.demo.BizServiceTest", and the mock container class is "com.demo.BizServiceTest.Mock":
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If the log file is no generated, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If only `com/demo/BizServiceTest$Mock` is found in the output, please check whether the mock class is created at correct place
- If both the test class and the mock class are found, but the class under test `com/demo/BizService` not appeared, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
- If all the three classes are found, and `Found method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
- If only the first line of `Handling mock class` is output, please check whether the mock class is created at correct place
- If `Handling mock class` and `Handling test class` are output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
For situations where expected mocking is not take effect, you could set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
For situations where expected mocking is not take effect, you could add a `@MockDiagnose` annotation to the mock class, and set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
```java
class DemoTest {
class BizServiceTest {
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
@@ -59,30 +52,30 @@ class DemoTest {
Executing the unit test again will print out the signatures of all mock methods, and the signatures of all invocations scanned in the class under test:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_mock_new_object"
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[TIP] Test case "should_mock_new_object"
... ...
[VERBOSE] Test case "should_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[TIP] Test case "should_set_mock_context"
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[TIP] Line 13, constructing "java.lang.Object()"
[INFO] Found method newFunc
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[INFO] Line 19, mock method "createBlackBox" used
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[INFO] Found method outerFunc
[TIP] Line 27, constructing "java.lang.StringBuilder()"
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[INFO] Line 27, mock method "innerFunc" used
... ...
```
@@ -92,3 +85,7 @@ The logs are formatted in follow pattern:
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
> In order to clearly distinguish between the type of method return value and the type of invoker, the method signature recorded in the log uses a method definition structure similar to `Kotlin`.
Comparing the actual signature of the original call with the signature defined by the mock method, the problem is usually found quickly.

View File

@@ -1,7 +1,9 @@
Fast Mocking
---
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
In unit testing, the main role of the mock method is to replace those methods with **need external dependencies**, **with time-consuming**, **has random results**, or other effects that affect the development of the test, but do not affect the key the logic to be tested. Generally speaking, a certain call needs to be mocked, which is usually only related to its own characteristics, and has nothing to do with the source of the invocation.
Based on the above information, `TestableMock` has designed a minimalist mock mechanism. Unlike the common mock tools that uses **class** as the definition granularity of mocking, and repeats the description of mock behavior in each test case, `TestableMock` allows each business class (class under test) to be associated with a set of reusable collection of mock methods (carried by the mock container class), following the principle of "convention over configuration", and mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
> In summary, there are two simple rules:
> - Mock non-constructive method, copy the original method definition to the mock class, add a `@MockMethod` annotation
@@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
The `TestableMock` convention is that when the `targetClass` parameter of the `@MockMethod` annotation is not defined, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
```java
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
@@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
}
```
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because `Kotlin` has made magical changes to the String type, the method under test in the `Kotlin` example adds a layer of encapsulation to the `BlackBox` class)
### 1.2 Mock the member method of the class under test itself
@@ -161,7 +163,17 @@ For complete code examples, see the `should_get_source_method_name()` and `shoul
### 3. Verify the sequence and parameters of the mock method being invoked
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
In test cases, you can use the `InvokeVerifier.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
### 4. Special instructions
> **Naming Conventions for Test Classes and Mock Containers**
>
> By default, `TestableMock` assumes that the <u>package path of the test class and the class under test are the same, and the name is `<ClassUnderTest>+Test`</u> (usually Java project with `Maven` or `Gradle` conform to this convention).
> At the same time, it is agreed that the mock container associated with the test class is <u>in its internal static class named `Mock`</u>, or <u>an independent class named `<ClassUnderTest>+Mock` under the same package path</u>.
>
> When the test class or mock container path does not follow to this convention, you can use the `@MockWith` annotation to specify it explicitly. For details, see [Use MockWith Annotation](en-us/doc/use-mock-with.md).
For more implementation details of `TestableMock`, please refer to the [Design and Principle](en-us/doc/design-and-mechanism.md) document.

View File

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

View File

@@ -0,0 +1,4 @@
Test Data Access Object Layer
---
Plan to coming in version `0.7`.

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

@@ -1,7 +1,7 @@
TestableMock简介
---
单元测试中的Mock方法通常是为了绕开那些依赖外部资源或无关功能的方法调用使得测试重点能够集中在需要验证和保障的代码逻辑上。某个调用需要被Mock往往只与其自身特征有关而与调用的来源无关。
单元测试中的Mock方法通常是为了绕开那些依赖外部资源或无关功能的方法调用使得测试重点能够集中在需要验证和保障的代码逻辑上。
在定义Mock方法时开发者真正关心的只有一件事"<u>这个调用在测试的时候要换成那个假的Mock方法</u>"。

View File

@@ -8,9 +8,9 @@ TestableMock的设计和原理
这种设计基于两项基本假设:
1. 同一个测试类里一个测试用例里需要Mock掉的方法在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
2. 需要Mock的调用都来自被测类的代码。此假设是符合单元测试初衷的即单元测试只应该关注当前单元内部行为,单元外的逻辑应该被替换为Mock
2. 每个单元测试只关注被测单元内部的逻辑,单元外的无关调用应该被替换为Mock。即需要被Mock的调用应该都在被测类的代码中。
据此通过约定来简化符合假设的单元测试场景,通过配置来支持其余复杂的使用场景。
据此通过约定来简化符合以上假设的单元测试场景,通过配置来支持其余复杂的使用场景。
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>。

View File

@@ -15,7 +15,15 @@
对于会修改类加载器或被测类字节码的Mock工具譬如`PowerMock``JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
#### 3. 父类变量指向子类对象时如何实现Mock方法
#### 3. `TestableMock`支持哪些测试框架
`TestableMock``PrivateAccessor`/`OmniConstructor`/`OmniAccessor`以及基础Mock功能均与测试框架无关理论上适用于任何测试框架。
唯独Mock方法的调用校验器是与测试框架相关的目前已支持`JUnit 4`/`JUnit 5`/`TestNG`/`Spock`四款主流框架。
若亲遇到在特定测试框架下的兼容问题,或希望增加其他框架的支持,请通过[Issues](https://github.com/alibaba/testable-mock/issues)告诉我们。
#### 4. 父类变量指向子类对象时如何实现Mock方法
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
@@ -25,7 +33,7 @@
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
#### 4. 如何Mock对于泛型方法(模板方法)
#### 5. 如何Mock对于泛型方法(模板方法)
与普通方法的Mock方法相同直接在Mock方法上使用相同的泛型参数即可。
@@ -33,25 +41,25 @@
> 由于JVM存在泛型擦除机制对于Java项目也可以直接使用`Object`类型替代泛型参数见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
#### 5. 如何Mock在内部类代码里的调用
#### 6. 如何Mock在内部类代码里的调用
在其所在外部类对应的Mock容器中定义所需的Mock方法即可。
参见Java和Kotlin示例中`DemoInnerClass`测试类的用例。
#### 6. 在Kotlin项目对`String`类中的方法进行Mock不生效
#### 7. 在Kotlin项目对`String`类中的方法进行Mock不生效
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`因此无论将Mock目标写为`kotlin.String``java.lang.String`均无法正常匹配到原始的被调用方法。
实际场景中需要对`String`类中的方法进行Mock的场景很少`TestableMock`暂未对这种情况做特别处理。
#### 7. `TestableMock`能否用于Android项目的测试
#### 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`框架。
#### 8. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
这个问题是由于系统ClassPath包含太多路径所致与是否使用`TestableMock`无关。但需要注意的是IntelliJ提供了两种辅助解决机制`JAR manifest``classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`

View File

@@ -1,45 +1,61 @@
全局运行参数
---
`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` |
| thread.pool.enhance.enable | 是否启用基于`TransmittableThreadLocal`的Mock上下文存储 | 可用值为:`true` / `false` |
参见`demo`目录各示例项目中的`testable.properties`文件。
## 2.修改配置文件位置
除了配置文件,`TestableMock`也支持通过引入`testable-agent`包时,在末尾加上一个`=`符号,然后连接额外参数来实现全局配置。这种方法与使用配置文件基本等效,因此通常无需使用,但有唯一特例是`configFile`配置项,该配置项可用于修改全局配置文件的位置。
| 参数 | 描述 | 可用值和示例 |
| ---- | ---- | ---- |
| logLevel | 修改全局日志级别 | 可用值为`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/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

@@ -1,47 +1,55 @@
快速构造复杂的参数对象
---
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,尤其遇到被测函数的入参类型结构复杂、没有合适的构造方法、成员对象使用私有内部类的时候,常规方法往往无处下手。为此`TestableMock`提供了`OmniConstructor``OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,面向对象的层层封装,在测试时就成为了初始化对象状态的重重阻碍。尤其遇到类型结构嵌套复杂、没有合适的构造方法、需要使用私有内部类等等状况时,常规手段往往显得力不从心
为此`TestableMock`提供了`OmniConstructor``OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
### 1. 一行代码构造任何对象
万能的对象构造器`OmniConstructor`有两个静态方法:
不论目标类型多么奇葩,呼唤`OmniConstructor`,马上递给您~ 万能的对象构造器`OmniConstructor`有两个静态方法:
- `newInstance(任意类型)` ➜ 指定任意类型,返回一个该类型的对象
- `newArray(任意类型, 数组大小)` ➜ 指定任意类型,返回一个该类型的数组
用法举例:
```java
// 构造一个ComplicatedClass类型的对象
ComplicatedClass obj = OmniConstructor.newInstance(ComplicatedClass.class);
// 构造一个ComplicatedClass[]类型容量为5的数组
ComplicatedClass[] arr = OmniConstructor.newArray(ComplicatedClass.class, 5);
// 构造一个WhatEverClass类型的对象
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
// 构造一个WhatEverClass[]类型容量为5的数组
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
```
值得一提的是,使用`OmniConstructor`构造出来的并非是一个所有成员值为`null`的简单空对象。该对象的所有成员以及所有成员的所有子成员,都会在构造时被依次递归赋值。相比直接`new`构造的对象,使用`OmniConstructor`能够确保对象完全初始化,无需担心测试过程中发生`NullPointerException`问题。
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在近期版本修复
不仅如此,`OmniConstructor`构造出的绝非是所有成员值为`null`的简单空对象,而是所有成员以及所有成员的所有子成员,都已经依次递归初始化的"丰满"对象。相比使`new`进行构造,`OmniConstructor`能够确保对象结构完整,避免测试数据部分初始化导致的`NullPointerException`问题。
```java
// 使用构造函数创建对象
Parent parent = new Parent();
// 内部成员未初始化直接访问报NullPointerException异常
// 内部成员未初始化直接访问报NullPointerException异常(❌)
parent.getChild().getGrandChild();
// 使用OmniConstructor创建对象
Parent parent = OmniConstructor.newInstance(Parent.class);
// 无需顾虑,安心访问任意子成员
// 无需顾虑,安心访问任意子成员(✅)
parent.getChild().getGrandChild().getContent();
```
> **注意 1** :在当前版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本修复
>
> **注意 2** :基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
> 但对于更复杂的情形,譬如遇到某些类型的构造方法会抛出异常或包含其他妨碍构造正常执行的语句时,对象构造可能会失败。
> 此时可通过[Testable全局配置](zh-cn/doc/javaagent-args.md)`omni.constructor.enhance.enable = true`启用`OmniConstructor`的字节码增强模式在该模式下任何Java类型皆可构造。
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值相比将`null`作为Mock方法的返回值使用完全初始化的对象能够更好保障测试的可靠性。
详见`java-demo``kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中的用例
`java-demo``kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中,详细展示了当目标类型有多层嵌套结构、构造方法无法正常使用,甚至没有公开的构造方法时,如何用`OmniConstructor`轻松创建所需对象
### 2. 一行代码访问任意深度成员
在单元测试中,有时会遇到一些结构复杂的参数对象,与特定测试用例有关的仅仅是该对象结构深处的个别几个属性和状态。`OmniAccessor`的灵感来自于`XML`语言中的`xpath`节点选择器,它有`get``set`两个主要的静态方法:
对于测试数据而言,即使是结构复杂的参数对象,与特定测试用例有关的通常也只是其中的部分属性和状态,然而要为这些深藏在对象结构内部的字段赋值有时却并非易事。
做为`PrivateAccessor`功能的加加加强版,`OmniAccessor`的灵感来自于`XML`语言中的[XPath节点选择器](https://www.w3school.com.cn/xpath/xpath_syntax.asp),它提供了`get``set`两个主要的静态方法:
- `get(任意对象, "访问路径")` ➜ 返回根据路径匹配搜索到的所有成员对象
- `set(任意对象, "访问路径", 新的值)` ➜ 根据路径匹配为指定位置的对象赋值
@@ -76,7 +84,7 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
- `{Children[]}`: 匹配所有类型是`Children`数组的子孙成员
- `{Child}/{GrandChild}`: 匹配所有类型是`Child`的子孙成员里,所有类型是`GrandChild`子成员
成员名和类型可以在路径上混用,但暂不支持在同一级路径指定成员名称又指定类型的写法
成员名和类型可以在路径上混用暂不支持在同一级路径同时指定成员名称和类型)
- `child/{GrandChild}`: 匹配名字为`child`的子孙成员里,所有类型是`GrandChild`的子成员
- `{Child}/grandChild/content`: 匹配所有类型是`Child`的子孙成员里,名为`grandChild`子成员里的,名为`content`的子成员
@@ -102,10 +110,8 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
### 3. 特别说明
> **你真的需要用到`OmniAccessor`吗?**
>
>
> `OmniAccessor`具有基于Fail-Fast机制的防代码重构能力当用户提供的访问路径无法匹配到任何成员时`OmniAccessor`将立即抛出`NoSuchMemberError`错误,使单元测试提前终止。然而相比常规的成员访问方式,`OmniAccessor`在IDE重构方面的支持依然偏弱。
>
> 对于复杂对象的内容赋值,大多数情况下,我们更推荐使用[构造者模式](https://developer.aliyun.com/article/705058)或者暴露Getter/Setter方法实现。这些常规手段虽然稍显笨拙尤其在需要为许多相似的成员批量赋值的时候但对业务逻辑的封装和重构都更加友好。
> 仅当原类型不适合改造,且没有其它可访问目标成员的方法时,`OmniAccessor`才是最后的终极手段。
>
> 出于相同的原因,我们并不推荐在除单元测试之外的场景使用`OmniAccessor`方式来读写业务类的成员字段(虽然技术上可行)。

View File

@@ -25,9 +25,9 @@
为此,`TestableMock``PrivateAccessor`进行了增强,赋予其编译期私有成员校验能力。这项功能默认关闭,需要通过`@EnablePrivateAccess`注解开启。(实际上是通过该注解的`verifyTargetOnCompile`参数控制,由于此参数默认值为`true`,因此只需在被测类上添加该注解即可启用私有成员校验)
**注意 1.**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
**注意 1**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
**注意 2.**`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
**注意 2**`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
**注意 3** 此机制目前只针对`Java`语言实现,对于`Kotlin`以及其他JVM方言均无效。
@@ -42,6 +42,8 @@
### 2.1 直接访问私有成员
> 由于IDE语法报错原因此特性计划在未来版本中移除建议采用`PrivateAccessor`方式
第二种方法,除了借助`PrivateAccessor`工具类以外,凡是使用了`@EnablePrivateAccess`注解的测试类还会被自动赋予以下“特殊能力”:
- 调用**被测类**的私有方法(包括静态方法)

View File

@@ -1,5 +1,43 @@
# Release Note
## 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)
- 修复一处`Gradle`运行单元测试的兼容问题issue-123
## 0.6.0
- 增加`OmniConstructor``OmniAccessor`,支持快速构建方法入参
- 修复一处`FRAME FULL`字节码处理的异常 (issue-117)
@@ -110,26 +148,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

@@ -1,12 +1,13 @@
使用TestableMock
---
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
`TestableMock`现在已不仅是一款轻量易上手的单元测试Mock工具更是以**简化Java单元测试**为目标的综合辅助工具,包含以下功能:
- [快速Mock任意调用](zh-cn/doc/use-mock.md)使被测类的任意方法调用快速替换为Mock方法实现"指哪换哪"解决传统Mock工具使用繁琐的问题
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- [快速构造参数对象](zh-cn/doc/omni-constructor.md):生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
- [辅助测试void方法](zh-cn/doc/test-void-method.md)利用Mock校验器对方法的内部逻辑进行检查解决无返回值方法难以实施单元测试的问题
- [快速测试数据库SQL](zh-cn/doc/verify-sql.md)通过内置常见数据库访问包的Mock实现解决数据访问层(DAO层)代码逻辑难以直接测试验证的问题
## 在Maven项目中使用
@@ -16,7 +17,7 @@
```xml
<properties>
<testable.version>0.6.0</testable.version>
<testable.version>0.6.7</testable.version>
</properties>
```
@@ -63,8 +64,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.6.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
testImplementation('com.alibaba.testable:testable-all:0.6.7')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.7')
}
```
@@ -78,7 +79,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,4 +93,22 @@ test {
> }
> ```
>
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
> 完整代码可参考`demo/android-demo`示例项目。
> 若项目使用`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>
> ```
>
> Gradle项目在`build.gradle`中添加`sourceCompatibility`属性,例如:
> ```groovy
> sourceCompatibility = '6' // 或7/8/9/...
> ```
>
> 完整代码可参考`demo/spock-demo`示例项目。

View File

@@ -3,51 +3,45 @@
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问。
若要排查Mock相关的问题只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。例如
为此,`TestableMock`会在项目构建目录下自动保存最后一次测试运行过程的Mock扫描日志。默认位置为`target/testable-agent.log`Maven项目`build/testable-agent.log`Gradle项目
```java
class DemoTest {
@MockDiagnose(LogLevel.ENABLE)
public static class Mock {
...
}
}
```
输出日志示例如下:
日志内容示例如下:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 27, mock method "innerFunc" used
[DIAGNOSE] Line 27, mock method "staticFunc" used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 34, mock method "trim" used
[DIAGNOSE] Line 34, mock method "sub" used
[DIAGNOSE] Line 34, mock method "startsWith" used
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
... ...
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[INFO] Found method newFunc
[INFO] Line 19, mock method "createBlackBox" used
[INFO] Found method outerFunc
[INFO] Line 27, mock method "innerFunc" used
[INFO] Line 27, mock method "staticFunc" used
[INFO] Found method commonFunc
[INFO] Line 34, mock method "trim" used
[INFO] Line 34, mock method "sub" used
[INFO] Line 34, mock method "startsWith" used
... ...
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
```
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
简单排查方法
依据需排查的测试类,进行针对性排查。假设被测类为"com.demo.BizService",测试类为"com.demo.BizServiceTest"Mock容器类为"com.demo.BizServiceTest.Mock"
-没有任何输出,请检查`pom.xml``build.gradle`配置是否正确引入了`TestableMock`依赖
-只输出了`Handling mock class`请检查Mock容器类的名称和位置是否符合规范
-只输出了`Handling mock class``Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
-输出了`Handling source class`以及`Handling method xxx`但预期的代码行位置没有发生Mock替换请继续检查Mock方法定义是否未与目标方法匹配
-该日志文件未生成,请检查`pom.xml``build.gradle`配置是否正确引入了`TestableMock`依赖
-日志中只能找到`com/demo/BizServiceTest$Mock`,没有找到被测类和测试类请检查Mock容器类的名称和位置是否符合规范
-日志中找到了Mock类和测试类但没有找到被测类`com/demo/BizService`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
-日志中三个类都已经找到,且有`Found method xxx`但预期的代码行位置没有发生Mock替换请继续检查Mock方法定义是否未与目标方法匹配
对于上述的最后一种情况预期Mock未生效日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
对于上述的最后一种情况预期Mock未生效使用`@MockDiagnose`注解将相应Mock容器类的日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
```java
class DemoTest {
class BizServiceTest {
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
@@ -55,39 +49,43 @@ class DemoTest {
}
```
再次执行单元测试,此时将会打印出所有Mock方法签名定义,以及被测类中扫描到所有调用的实际方法签名:
再次执行单元测试,此时日志将会包含所有Mock类中的方法签名定义,以及被测类中扫描到所有调用的实际方法签名:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_mock_new_object"
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[TIP] Test case "should_mock_new_object"
... ...
[VERBOSE] Test case "should_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[TIP] Test case "should_set_mock_context"
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[TIP] Line 13, constructing "java.lang.Object()"
[INFO] Found method newFunc
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[INFO] Line 19, mock method "createBlackBox" used
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[INFO] Found method outerFunc
[TIP] Line 27, constructing "java.lang.StringBuilder()"
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[INFO] Line 27, mock method "innerFunc" used
... ...
```
输出日志结构参考如下:
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"`测试类中扫描到的**Mock构造方法**及其签名
- `Mock method "<Mock方法名>" as "<方法签名>"`测试类中扫描到的**普通Mock方法**及其签名
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"`Mock类中扫描到的**Mock构造方法**及其签名
- `Mock method "<Mock方法名>" as "<方法签名>"`Mock类中扫描到的**普通Mock方法**及其签名
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
> 为了便于清楚的区分返回值类型和调用目标类型,日志中记录的方法签名采用了类似`Kotlin`的方法定义结构。
对比原调用的实际签名和Mock方法定义的签名通常很快就能够找出问题所在。

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

@@ -1,13 +1,15 @@
快速Mock被测类的任意方法调用
---
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用
在单元测试中Mock方法的主要作用是替代某些**需要外部依赖**、**执行过程耗时**、**执行结果随机**或其他影响测试正常开展,却并不影响关键待测逻辑的调用。通常来说,某个调用需要Mock,往往只与其自身特征有关,而与调用的来源无关
> 规则归纳起来就两条:
基于上述特点,`TestableMock`设计了一种极简的Mock机制。与以往Mock工具以**类**作为Mock的定义粒度在每个测试用例里各自重复描述Mock行为的方式不同`TestableMock`让每个业务类被测类关联一组可复用的Mock方法集合使用Mock容器类承载并遵循约定优于配置的原则按照规则自动在测试运行时替换被测类中的指定方法调用。
> 实际规则约定归纳起来只有两条:
> - Mock非构造方法拷贝原方法定义到Mock容器类加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到Mock容器类返回值换成构造的类型方法名随意加`@MockContructor`注解
具体的Mock方法定义约定如下。
具体使用方法如下。
### 0. 前置步骤准备Mock容器
@@ -29,11 +31,11 @@ public class DemoTest {
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
例如,被测类中有一处`"anything".substring(1, 2)`调用我们希望在运行测试的时候将它换成一个固定字符串则只需在Mock容器类定义如下方法
例如,被测类中有一处`"something".substring(0, 4)`调用我们希望在运行测试的时候将它换成一个固定字符串则只需在Mock容器类定义如下方法
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 调用此方法的对象`"something"`类型为`String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
return "sub_string";
@@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
有时在Mock方法里会需要访问发起调用的原始对象中的成员变量或是调用原始对象的其他方法。此时可以将`@MockMethod`注解中的`targetClass`参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数值为空Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为`self``src`。举例如下:
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数未定义Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为`self``src`。举例如下:
```java
// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
@@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于`Kotlin`对String类型进行了魔改`Kotlin`示例中将被测方法在`BlackBox`类里加了一层封装)
### 1.2 覆写被测类自身的成员方法
@@ -161,16 +163,15 @@ private Data mockDemo() {
### 3. 验证Mock方法被调用的顺序和参数
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()``withInOrder()``without()``withTimes()`等方法实现对Mock调用情况的验证。
在测试用例中可用通过`InvokeVerifier.verify()`方法,配合`with()``withInOrder()``without()``withTimes()`等方法实现对Mock调用情况的验证。
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
### 4. 特别说明
> **Mock只对被测类的代码有效**
>
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>哦(~ ̄▽ ̄)。知道大家是想快速验证一下`TestableMock`的功能,不过测试用例的代码真无需被Mock(这心意我们领了👻)
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>。知道大家是想快速验证一下`TestableMock`的功能(这心意我们领了👻),不过测试用例的代码真无需被Mock哦(~ ̄▽ ̄)
>
> 除去这种情况若Mock未生效请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u>和<u>目标位置的调用方法签名</u>。若依然无法定位原因欢迎提交Issues告诉我们。
@@ -180,3 +181,5 @@ private Data mockDemo() {
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>。
>
> 当测试类或Mock容器路径不符合此约定时可使用`@MockWith`注解显式指定,详见[使用MockWith注解](zh-cn/doc/use-mock-with.md)。
关于`TestableMock`的更多实现细节可参考[设计和原理](zh-cn/doc/design-and-mechanism.md)文档。

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,6 @@
测试数据访问层逻辑
---
由于数据库的访问本质上属于外部调用因此在单元测试中往往被作为Mock的目标导致DAO层逻辑很容易成为单元测试盲区。为此`TestableMock`通过针对数据库访问包内部逻辑的精准Mock提供按需拦截和验证SQL语句的功能。
计划在`0.7`版本中推出面向`Mybatis`的支持。

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