Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0917406c44 | ||
|
|
3bf19ee6d0 | ||
|
|
e1afb1cd54 | ||
|
|
10953ff02a | ||
|
|
80b0f3b878 | ||
|
|
8fbd459280 | ||
|
|
f3309f933e | ||
|
|
b47204e329 | ||
|
|
6616ff1174 | ||
|
|
09ecd0d7c0 | ||
|
|
93db467f43 | ||
|
|
a78931ae81 | ||
|
|
93ad1fa94f | ||
|
|
2780ccc6fa | ||
|
|
fd3b088f59 | ||
|
|
e812aa6126 | ||
|
|
b949f57d54 | ||
|
|
0551240c7d | ||
|
|
8a3a0cf6bf | ||
|
|
d3ab246c78 | ||
|
|
142b0e5a1e | ||
|
|
38221a67ef | ||
|
|
993c178adf | ||
|
|
3786c6251e | ||
|
|
e5f8e5244c | ||
|
|
e13dce7ac6 | ||
|
|
3265803dec | ||
|
|
ef3891bd57 | ||
|
|
fcbdb7275b | ||
|
|
ea4c6c5b3e | ||
|
|
381ff52515 | ||
|
|
c5f39dfae3 | ||
|
|
7f8ef70e57 | ||
|
|
23af64475a | ||
|
|
7181b49211 | ||
|
|
cd7b9cc922 | ||
|
|
46288fa0e9 | ||
|
|
e61597ba8b | ||
|
|
c577301493 | ||
|
|
a33908db41 | ||
|
|
4ea97fe13f | ||
|
|
7cda8ffd36 | ||
|
|
1da98a0364 | ||
|
|
c6d892da2d | ||
|
|
d1f2c3da97 | ||
|
|
f40cfe929a | ||
|
|
96d1f9d633 | ||
|
|
350802fc9b | ||
|
|
0ae5163435 | ||
|
|
0e581a7eeb | ||
|
|
b68ff99800 | ||
|
|
7754f828cc | ||
|
|
3d62c91291 | ||
|
|
14c3d841c5 | ||
|
|
5f6cf3888c | ||
|
|
75c6a67b8f | ||
|
|
d11402dd6c | ||
|
|
db771b9c66 | ||
|
|
338c7828c8 | ||
|
|
8ebe2a5d0c | ||
|
|
4f3e3f0093 | ||
|
|
9924d7d2fc | ||
|
|
9f5867ed76 | ||
|
|
be0a580a04 | ||
|
|
78d7ee21a6 | ||
|
|
af409a65a8 | ||
|
|
a67619d574 | ||
|
|
b89daa22c3 | ||
|
|
e948e625ea | ||
|
|
d463f863db | ||
|
|
31140bef87 | ||
|
|
d2944a88fb | ||
|
|
9d6e4dd1fa | ||
|
|
9c09bf9653 | ||
|
|
6d87653fa2 | ||
|
|
eec88f4bd9 | ||
|
|
5921ccab07 | ||
|
|
8dde402a8c | ||
|
|
d10478f95f | ||
|
|
0c063e9b1f | ||
|
|
b8771f0dac | ||
|
|
42dd58584b | ||
|
|
ce55bb4c9a | ||
|
|
cff2d1ae5a | ||
|
|
5904ad99cb | ||
|
|
74fbc5b53f | ||
|
|
f6b5bab250 | ||
|
|
f2feb3cc68 | ||
|
|
b41923bfa0 | ||
|
|
7c1f18de46 | ||
|
|
c393d40d0d | ||
|
|
d4a69528a7 | ||
|
|
a46e73a421 | ||
|
|
01e9890234 | ||
|
|
93cdef1d6c | ||
|
|
e2e986e50d | ||
|
|
f5475aa18e | ||
|
|
844323af3a | ||
|
|
cc2deeff1c | ||
|
|
14e4d2db5b | ||
|
|
c9c8ba6c24 | ||
|
|
b5c0afb3da | ||
|
|
617d372545 | ||
|
|
58da25de53 | ||
|
|
46706ba56c | ||
|
|
1ced04bf29 | ||
|
|
7b2f967626 | ||
|
|
e0a4a39d55 | ||
|
|
b4da23a90e | ||
|
|
c1659242b6 | ||
|
|
929abffb66 |
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# maven ignore
|
||||
target/
|
||||
.mvn/
|
||||
|
||||
# gradle ignore
|
||||
build/
|
||||
|
||||
11
README.md
@@ -11,17 +11,13 @@
|
||||
|
||||
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
## 项目维护说明
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
由于当前并行项目较多,此项目暂时转入维护阶段,在此期间`TestableMock`会继续提供不定期的版本更新。
|
||||
|
||||
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.7` 完善Mock功能的防代码重构机制,支持快速Mock指定类型的所有方法
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将尽快回复并处理。
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -32,6 +28,7 @@
|
||||
|-- testable-agent ➜ JavaAgent模块,提供Mock测试相关功能
|
||||
|-- testable-core ➜ 基础功能模块,提供Mock相关注解和工具类
|
||||
|-- testable-maven-plugin ➜ Maven插件模块,用于简化JavaAgent注入
|
||||
|-- tool ➜ 项目开发过程中的工具脚本
|
||||
|-- demo
|
||||
| |-- java-demo ➜ Java语言的示例代码
|
||||
| `-- kotlin-demo ➜ Kotlin语言的示例代码
|
||||
|
||||
@@ -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
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
1
demo/android-demo/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
55
demo/android-demo/app/build.gradle
Normal file
@@ -0,0 +1,55 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.alibaba.testable.demo"
|
||||
minSdkVersion 30
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
all {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-rc01'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
testImplementation 'androidx.test:core:1.4.0-alpha05'
|
||||
testImplementation 'androidx.test:runner:1.4.0-alpha05'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
testImplementation 'com.alibaba.testable:testable-all:0.6.9'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
21
demo/android-demo/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.alibaba.testable.demo", appContext.packageName)
|
||||
}
|
||||
}
|
||||
13
demo/android-demo/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.alibaba.testable.demo">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TestableAndroidDemo" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
public class DemoBasic {
|
||||
|
||||
/**
|
||||
* method with new operation
|
||||
*/
|
||||
public String newFunc() {
|
||||
BlackBox component = new BlackBox("something");
|
||||
return component.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* method with member method invoke
|
||||
*/
|
||||
public String outerFunc(String s) throws Exception {
|
||||
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* method with common method invoke
|
||||
*/
|
||||
public String commonFunc() {
|
||||
return "anything".trim() + "__" + "anything".substring(1, 2) + "__" + "abc".startsWith("ab");
|
||||
}
|
||||
|
||||
/**
|
||||
* method with static method invoke
|
||||
*/
|
||||
public BlackBox getBox() {
|
||||
return BlackBox.secretBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
public String callerOne() {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
|
||||
public String callerTwo() {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
|
||||
private static String staticFunc() {
|
||||
return "_STATIC_TAIL";
|
||||
}
|
||||
|
||||
private String innerFunc(String s) {
|
||||
return "Nothing";
|
||||
}
|
||||
|
||||
private String callFromDifferentMethod() {
|
||||
return "realOne";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* 演示与Robolectric配合使用
|
||||
* Demonstrate using mock along with Robolectric
|
||||
*/
|
||||
public class DemoService extends Service {
|
||||
|
||||
static final String TAG = "DemoService";
|
||||
SharedPreferences mPreference;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if(intent != null && "start_foreground".equalsIgnoreCase(intent.getAction())) {
|
||||
Log.d(TAG, "start service.");
|
||||
} else if(intent != null && "stop_foreground".equalsIgnoreCase(intent.getAction())) {
|
||||
Log.d(TAG, "stop service.");
|
||||
}
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
public BlackBox(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static BlackBox secretBox() {
|
||||
return new BlackBox("secret");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String something) {
|
||||
data = something;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor() {
|
||||
return "black";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
protected String data;
|
||||
|
||||
abstract public void put(String something);
|
||||
|
||||
public String get() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
String getColor();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
demo/android-demo/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demo/android-demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
16
demo/android-demo/app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.TestableAndroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
10
demo/android-demo/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
demo/android-demo/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Testable Android Demo</string>
|
||||
</resources>
|
||||
16
demo/android-demo/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.TestableAndroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
public class DemoBasicTest {
|
||||
|
||||
private DemoBasic demoBasic = new DemoBasic();
|
||||
|
||||
public static class Mock {
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoBasic.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne":
|
||||
return "mock_one";
|
||||
default:
|
||||
return "mock_others";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_new_object() {
|
||||
assertEquals("mock_something", demoBasic.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoBasic.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
verify("staticFunc").with();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoBasic.commonFunc());
|
||||
verify("trim").withTimes(1);
|
||||
verify("sub").withTimes(1);
|
||||
verify("startsWith").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_mock_static_method() {
|
||||
assertEquals("not_secret_box", demoBasic.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_get_source_method_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoBasic.callerOne() + "_" + demoBasic.callerTwo());
|
||||
// asynchronous
|
||||
assertEquals("mock_one_mock_others",
|
||||
Executors.newSingleThreadExecutor().submit(() -> demoBasic.callerOne() + "_" + demoBasic.callerTwo()).get());
|
||||
verify("callFromDifferentMethod").withTimes(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoBasic.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoBasic.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 30)
|
||||
public class DemoServiceTest {
|
||||
|
||||
private DemoService demoService;
|
||||
|
||||
public static class Mock {
|
||||
@MockMethod(targetClass = Log.class, targetMethod = "d")
|
||||
public static int log(String tag, String msg) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
demoService = Robolectric.setupService(DemoService.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartCommand() {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.setAction("start_foreground");
|
||||
demoService.onStartCommand(intent, 0, 1);
|
||||
verify("log").with("DemoService", "start service.");
|
||||
|
||||
intent.setAction("stop_foreground");
|
||||
demoService.onStartCommand(intent, 0, 1);
|
||||
verify("log").with("DemoService", "stop service.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
log.file = null
|
||||
28
demo/android-demo/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.4.32"
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.0.0-alpha14"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
21
demo/android-demo/gradle.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
6
demo/android-demo/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Fri Apr 30 14:35:40 CST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
172
demo/android-demo/gradlew
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
demo/android-demo/gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
2
demo/android-demo/settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
include ':app'
|
||||
rootProject.name = "Testable Android Demo"
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.9')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
<testable.version>0.6.9</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
/**
|
||||
* 目标类,此类中的一些调用将会被Mock掉
|
||||
* Target class, some invocations inside this class will be mocked
|
||||
*/
|
||||
public class CookerService {
|
||||
|
||||
private static String hireSandwichCooker() {
|
||||
return "Real-Sandwich-Cooker";
|
||||
}
|
||||
|
||||
private static String hireHamburgerCooker() {
|
||||
return "Real-Hamburger-Cooker";
|
||||
}
|
||||
|
||||
private String cookSandwich() {
|
||||
return "Real-Sandwich";
|
||||
}
|
||||
|
||||
private String cookHamburger() {
|
||||
return "Real-Hamburger";
|
||||
}
|
||||
|
||||
public String prepareSandwich() {
|
||||
return hireSandwichCooker() + " & " + cookSandwich();
|
||||
}
|
||||
|
||||
public String prepareHamburger() {
|
||||
return hireHamburgerCooker() + " & " + cookHamburger();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
/**
|
||||
* 被测类,会访问`CookerService`里的方法
|
||||
* Class to be tested, which will access methods in TargetService class
|
||||
*/
|
||||
public class SellerService {
|
||||
|
||||
private CookerService cookerService = new CookerService();
|
||||
|
||||
public String sellSandwich() {
|
||||
return cookerService.prepareSandwich();
|
||||
}
|
||||
|
||||
public String sellHamburger() {
|
||||
return cookerService.prepareHamburger();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,9 @@ public class DemoPrivateAccess {
|
||||
* private member method with arguments
|
||||
*/
|
||||
private String privateFuncWithArgs(List<String> list, String str, int i) {
|
||||
return list.stream().reduce((a, s) -> a + s).orElse("") + " + " + str + " + " + i;
|
||||
return list.stream().reduce((a, s) -> a + s).orElse("")
|
||||
+ " + " + (str == null ? "null" : str)
|
||||
+ " + " + i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.lambda;
|
||||
|
||||
/**
|
||||
* @author jim
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Function1Throwable<T, R> {
|
||||
R apply(T t) throws Throwable;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package com.alibaba.demo.lambda;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author jim
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LambdaDemo {
|
||||
|
||||
public void methodReference() {
|
||||
consumesRun(this::run);
|
||||
}
|
||||
|
||||
private void consumesRun(Runnable r) {
|
||||
r.run();
|
||||
}
|
||||
|
||||
private void run() {
|
||||
blackHole();
|
||||
}
|
||||
|
||||
public String methodReference0() {
|
||||
return consumes0(this::function0);
|
||||
}
|
||||
|
||||
private String consumes0(Supplier<String> function0) {
|
||||
return function0.get();
|
||||
}
|
||||
|
||||
private String function0() {
|
||||
return "Hello";
|
||||
}
|
||||
|
||||
public String methodReference1() {
|
||||
return consumes1(this::function1);
|
||||
}
|
||||
|
||||
private String consumes1(Function<Integer, String> function) {
|
||||
return function.apply(1);
|
||||
}
|
||||
|
||||
private String function1(Integer i) {
|
||||
return String.valueOf(i);
|
||||
}
|
||||
|
||||
public String methodReferenceThrows() {
|
||||
return consumesThrows(this::function1Throwable);
|
||||
}
|
||||
|
||||
private String consumesThrows(Function1Throwable<Integer, String> function) {
|
||||
try {
|
||||
return function.apply(1);
|
||||
}catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
private String function1Throwable(Integer i) throws Throwable{
|
||||
return String.valueOf(i);
|
||||
}
|
||||
|
||||
public String methodReference2() {
|
||||
return consumes2(this::function2);
|
||||
}
|
||||
|
||||
private String consumes2(BiFunction<Integer, Double, String> function) {
|
||||
return function.apply(1, .2);
|
||||
}
|
||||
|
||||
private String function2(Integer i, Double d) {
|
||||
return i + String.valueOf(d);
|
||||
}
|
||||
|
||||
public String staticMethodReference1() {
|
||||
return consumes1(StaticMethod::function1);
|
||||
}
|
||||
|
||||
public String staticMethodReference2() {
|
||||
return consumes2(StaticMethod::function2);
|
||||
}
|
||||
|
||||
public void lambdaRun() {
|
||||
consumes(() -> System.out.println("lambdaRun"));
|
||||
}
|
||||
|
||||
private void consumes(Runnable o) {
|
||||
o.run();
|
||||
}
|
||||
|
||||
public void methodReferenceNew() {
|
||||
Object o = consumes(Object::new);
|
||||
blackHole(o);
|
||||
}
|
||||
|
||||
private <T> T consumes(Supplier<T> s) {
|
||||
return s.get();
|
||||
}
|
||||
|
||||
private void blackHole(Object... ignore) {}
|
||||
|
||||
public void array() {
|
||||
Function<Boolean[], Boolean[]> arrayBooleanFunction = this::arrayBooleanFunction;
|
||||
Function<boolean[], boolean[]> arrayBooleanFunction1 = this::arrayBoolFunction;
|
||||
Function<Byte[], Byte[]> byteFunction = this::arrayByteFunction;
|
||||
Function<byte[], byte[]> byteFunction1 = this::arrayByteFunction;
|
||||
Function<Character[], Character[]> charFunction = this::arrayCharFunction;
|
||||
Function<char[], char[]> charFunction1 = this::arrayCharFunction;
|
||||
Function<Short[], Short[]> shortFunction = this::arrayShortFunction;
|
||||
Function<short[], short[]> shortFunction1 = this::arrayShortFunction;
|
||||
Function<int[], int[]> intFunction = this::arrayIntFunction;
|
||||
Function<Integer[], Integer[]> intFunction1 = this::arrayIntegerFunction;
|
||||
Function<long[], long[]> longFunction = this::arrayLongFunction;
|
||||
Function<Long[], Long[]> longFunction1 = this::arrayLongFunction;
|
||||
Function<Float[], Float[]> floatFunction = this::arrayFloatFunction;
|
||||
Function<float[], float[]> floatFunction1 = this::arrayFloatFunction;
|
||||
Function<Double[], Double[]> doubleFunction = this::arrayDoubleFunction;
|
||||
Function<double[], double[]> doubleFunction1 = this::arrayDoubleFunction;
|
||||
blackHole(arrayBooleanFunction, arrayBooleanFunction1,
|
||||
byteFunction, byteFunction1, charFunction, charFunction1, shortFunction, shortFunction1,
|
||||
intFunction, intFunction1, longFunction, longFunction1, floatFunction, floatFunction1, doubleFunction,
|
||||
doubleFunction1
|
||||
);
|
||||
}
|
||||
|
||||
private int[] arrayIntFunction(int[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Integer[] arrayIntegerFunction(Integer[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private boolean[] arrayBoolFunction(boolean[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Boolean[] arrayBooleanFunction(Boolean[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private byte[] arrayByteFunction(byte[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Byte[] arrayByteFunction(Byte[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private char[] arrayCharFunction(char[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Character[] arrayCharFunction(Character[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private short[] arrayShortFunction(short[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Short[] arrayShortFunction(Short[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private long[] arrayLongFunction(long[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Long[] arrayLongFunction(Long[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private float[] arrayFloatFunction(float[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Float[] arrayFloatFunction(Float[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private double[] arrayDoubleFunction(double[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
private Double[] arrayDoubleFunction(Double[] arg) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
public void generic() {
|
||||
Function<?, ?> genericFunction = this::genericFunction;
|
||||
blackHole(genericFunction);
|
||||
}
|
||||
|
||||
public <T, R> R genericFunction(T arg) {
|
||||
//noinspection unchecked
|
||||
return (R)arg;
|
||||
}
|
||||
|
||||
private void collects() {
|
||||
long l = Stream.of("1", "2", "3")
|
||||
.filter(v -> !"2".equals(v))
|
||||
.map(Long::parseLong)
|
||||
.peek(this::blackHole)
|
||||
.map(v -> new ArrayList<Long>(){{add(v);}})
|
||||
.flatMap(Collection::stream)
|
||||
.mapToLong(Long::valueOf)
|
||||
.sum();
|
||||
blackHole(l);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.alibaba.demo.lambda;
|
||||
|
||||
/**
|
||||
* @author jim
|
||||
*/
|
||||
public class StaticMethod {
|
||||
|
||||
public static String function1(Integer i) {
|
||||
return "static" + i;
|
||||
}
|
||||
|
||||
public static String function2(Integer i, Double d) {
|
||||
return "static" + i + d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
|
||||
class CookerServiceMock {
|
||||
|
||||
@MockMethod(targetClass = CookerService.class)
|
||||
public static String hireSandwichCooker() {
|
||||
return "Fake-Sandwich-Cooker";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class, scope = MockScope.ASSOCIATED)
|
||||
public static String hireHamburgerCooker() {
|
||||
return "Fake-Hamburger-Cooker";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class)
|
||||
private String cookSandwich() {
|
||||
return "Faked-Sandwich";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService.class, scope = MockScope.ASSOCIATED)
|
||||
private String cookHamburger() {
|
||||
return "Faked-Hamburger";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.demo.association;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SellerServiceTest {
|
||||
|
||||
private SellerService sellerService = new SellerService();
|
||||
|
||||
@Test
|
||||
void should_sell_sandwich() {
|
||||
assertEquals("Fake-Sandwich-Cooker & Faked-Sandwich", sellerService.sellSandwich());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_sell_hamburger() {
|
||||
assertEquals("Real-Hamburger-Cooker & Real-Hamburger", sellerService.sellHamburger());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.alibaba.demo.basic;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -94,7 +93,7 @@ class DemoMockTest {
|
||||
|
||||
@Test
|
||||
void should_mock_static_method() {
|
||||
Assertions.assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.core.tool.PrivateAccessor.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@@ -20,39 +20,45 @@ 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"));
|
||||
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
String value = invoke(demoPrivateAccess, "privateFuncWithArgs", list, null, 0);
|
||||
assertEquals("abc + null + 0", value);
|
||||
|
||||
value = invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1);
|
||||
assertEquals("null + 1", value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.alibaba.demo.lambda;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockDiagnose;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.model.LogLevel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* @author zcbbpo
|
||||
*/
|
||||
public class LambdaDemoTest {
|
||||
private LambdaDemo lambdaDemo = new LambdaDemo();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
@MockMethod(targetClass = LambdaDemo.class, targetMethod = "run")
|
||||
private void mockRun() {
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = LambdaDemo.class)
|
||||
private String function0() {
|
||||
return "mock_function0";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = LambdaDemo.class)
|
||||
private String function1(Integer i) {
|
||||
return "mock_function1";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = LambdaDemo.class)
|
||||
private String function2(Integer i, Double d) {
|
||||
return "mock_function2";
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
@MockMethod(targetClass = LambdaDemo.class)
|
||||
private String function1Throwable(Integer i) throws Throwable{
|
||||
return "mock_function1Throwable";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = StaticMethod.class, targetMethod = "function1")
|
||||
public static String staticFunction1(Integer i) {
|
||||
return "mock_staticFunction1";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockRun() {
|
||||
lambdaDemo.methodReference();
|
||||
verify("mockRun").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockFunction0() {
|
||||
String s = lambdaDemo.methodReference0();
|
||||
assertEquals(s, "mock_function0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockFunction1() {
|
||||
String s = lambdaDemo.methodReference1();
|
||||
assertEquals(s, "mock_function1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockFunction2() {
|
||||
String s = lambdaDemo.methodReference2();
|
||||
assertEquals(s, "mock_function2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockFunction1Throws() {
|
||||
String s = lambdaDemo.methodReferenceThrows();
|
||||
assertEquals(s, "mock_function1Throwable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMockStaticFunction1() {
|
||||
String s = lambdaDemo.staticMethodReference1();
|
||||
assertEquals(s, "mock_staticFunction1");
|
||||
}
|
||||
|
||||
}
|
||||
2
demo/java-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
omni.constructor.enhance.enable = true
|
||||
mock.target.checking.enable = true
|
||||
@@ -9,6 +9,7 @@ version = "1.0.0-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -16,8 +17,8 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.0")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.9")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.9")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
<testable.version>0.6.9</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = "demo"
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
/**
|
||||
* 目标类,此类中的一些调用将会被Mock掉
|
||||
* Target class, some invocations inside this class will be mocked
|
||||
*/
|
||||
class CookerService {
|
||||
|
||||
private fun cookSandwich(): String {
|
||||
return "Real-Sandwich"
|
||||
}
|
||||
|
||||
private fun cookHamburger(): String {
|
||||
return "Real-Hamburger"
|
||||
}
|
||||
|
||||
fun prepareSandwich(): String {
|
||||
return hireSandwichCooker() + " & " + cookSandwich()
|
||||
}
|
||||
|
||||
fun prepareHamburger(): String {
|
||||
return hireHamburgerCooker() + " & " + cookHamburger()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun hireSandwichCooker(): String {
|
||||
return "Real-Sandwich-Cooker"
|
||||
}
|
||||
|
||||
private fun hireHamburgerCooker(): String {
|
||||
return "Real-Hamburger-Cooker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
/**
|
||||
* 被测类,会访问`CookerService`里的方法
|
||||
* Class to be tested, which will access methods in TargetService class
|
||||
*/
|
||||
class SellerService {
|
||||
|
||||
private val cookerService = CookerService()
|
||||
|
||||
fun sellSandwich(): String {
|
||||
return cookerService.prepareSandwich()
|
||||
}
|
||||
|
||||
fun sellHamburger(): String {
|
||||
return cookerService.prepareHamburger()
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.model.MockScope
|
||||
|
||||
internal class CookerServiceMock {
|
||||
|
||||
@MockMethod(targetClass = CookerService::class)
|
||||
private fun cookSandwich(): String {
|
||||
return "Faked-Sandwich"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class, scope = MockScope.ASSOCIATED)
|
||||
private fun cookHamburger(): String {
|
||||
return "Faked-Hamburger"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class)
|
||||
fun hireSandwichCooker(): String {
|
||||
return "Fake-Sandwich-Cooker"
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = CookerService::class, scope = MockScope.ASSOCIATED)
|
||||
fun hireHamburgerCooker(): String {
|
||||
return "Fake-Hamburger-Cooker"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alibaba.demo.association
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SellerServiceTest {
|
||||
|
||||
private val sellerService = SellerService()
|
||||
|
||||
@Test
|
||||
fun should_sell_sandwich() {
|
||||
assertEquals("Fake-Sandwich-Cooker & Faked-Sandwich", sellerService.sellSandwich())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_sell_hamburger() {
|
||||
assertEquals("Real-Hamburger-Cooker & Real-Hamburger", sellerService.sellHamburger())
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.Box
|
||||
import com.alibaba.demo.basic.model.mock.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = com.alibaba.demo.basic.DemoInherit()
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
@@ -52,41 +52,41 @@ internal class DemoInheritTest {
|
||||
fun should_mock_call_sub_object_method_by_parent_object() {
|
||||
val box = demoInherit.putIntoBox() as BlackBox
|
||||
InvokeVerifier.verify("put_into_box").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_box", box.get())
|
||||
assertEquals("put_data_into_box", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_sub_object_method_by_sub_object() {
|
||||
val box = demoInherit.putIntoBlackBox()
|
||||
InvokeVerifier.verify("put_into_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_blackbox", box.get())
|
||||
assertEquals("put_data_into_blackbox", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_parent_object_method_by_parent_object() {
|
||||
val content = demoInherit.fromBox
|
||||
InvokeVerifier.verify("get_from_box").withTimes(1)
|
||||
Assertions.assertEquals("get_from_box", content)
|
||||
assertEquals("get_from_box", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_parent_object_method_by_sub_object() {
|
||||
val content = demoInherit.fromBlackBox
|
||||
InvokeVerifier.verify("get_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("get_from_blackbox", content)
|
||||
assertEquals("get_from_blackbox", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_interface_method_by_interface_object() {
|
||||
val color = demoInherit.colorViaColor
|
||||
InvokeVerifier.verify("get_color_from_color").withTimes(1)
|
||||
Assertions.assertEquals("color_from_color", color)
|
||||
assertEquals("color_from_color", color)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_call_interface_method_by_sub_class_object() {
|
||||
val color = demoInherit.colorViaBox
|
||||
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("color_from_blackbox", color)
|
||||
assertEquals("color_from_blackbox", color)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -21,8 +21,8 @@ internal class DemoInnerClassTest {
|
||||
@Throws(Exception::class)
|
||||
fun should_mock_invoke_inside_inner_class() {
|
||||
val demo = DemoInnerClass()
|
||||
Assertions.assertEquals("MockedCall", demo.callInnerDemo())
|
||||
Assertions.assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
Assertions.assertEquals("MockedCall", DemoInnerClass.StaticInner().demo())
|
||||
assertEquals("MockedCall", demo.callInnerDemo())
|
||||
assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
assertEquals("MockedCall", DemoInnerClass.StaticInner().demo())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = com.alibaba.demo.basic.DemoMatcher()
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: com.alibaba.demo.basic.DemoMatcher) {
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: com.alibaba.demo.basic.DemoMatcher, a1: Any, a2: Any) {
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: com.alibaba.demo.basic.DemoMatcher, a: Array<Any>) {
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import com.alibaba.demo.basic.model.omni.Child
|
||||
import com.alibaba.demo.basic.model.omni.Parent
|
||||
import com.alibaba.testable.core.tool.OmniAccessor
|
||||
import com.alibaba.testable.core.tool.OmniConstructor
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@@ -18,14 +19,14 @@ internal class DemoOmniMethodsTest {
|
||||
val parent = OmniConstructor.newInstance(Parent::class.java)
|
||||
|
||||
// 任意深度的子孙成员对象都不为空
|
||||
Assertions.assertNotNull(parent.child?.grandChild?.content)
|
||||
assertNotNull(parent.child?.grandChild?.content)
|
||||
|
||||
// 所有基础类型初始化为默认数值
|
||||
Assertions.assertEquals(0, parent.child?.grandChild?.value)
|
||||
Assertions.assertEquals("", parent.child?.grandChild?.content)
|
||||
assertEquals(0, parent.child?.grandChild?.value)
|
||||
assertEquals("", parent.child?.grandChild?.content)
|
||||
|
||||
// 所有数组类型初始化为空数组
|
||||
Assertions.assertEquals(0, parent.children?.size)
|
||||
assertEquals(0, parent.children?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -39,23 +40,23 @@ internal class DemoOmniMethodsTest {
|
||||
|
||||
// 使用成员名称快速读取成员对象
|
||||
var contents = OmniAccessor.get<String?>(parent, "content")
|
||||
Assertions.assertEquals(4, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
Assertions.assertEquals("from 1st children", contents[1])
|
||||
Assertions.assertEquals("from 2nd children", contents[2])
|
||||
Assertions.assertEquals("from 3rd children", contents[3])
|
||||
assertEquals(4, contents.size)
|
||||
assertEquals("from child", contents[0])
|
||||
assertEquals("from 1st children", contents[1])
|
||||
assertEquals("from 2nd children", contents[2])
|
||||
assertEquals("from 3rd children", contents[3])
|
||||
|
||||
// 使用成员类型快速读取成员对象
|
||||
contents = OmniAccessor.get(parent, "{Child}/{GrandChild}/content")
|
||||
Assertions.assertEquals(1, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
assertEquals(1, contents.size)
|
||||
assertEquals("from child", contents[0])
|
||||
|
||||
// 使用带下标的路径读取成员对象
|
||||
Assertions.assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"))
|
||||
Assertions.assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"))
|
||||
assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"))
|
||||
assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"))
|
||||
|
||||
// 使用模糊路径快速读取成员对象
|
||||
Assertions.assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"))
|
||||
assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -65,22 +66,22 @@ internal class DemoOmniMethodsTest {
|
||||
|
||||
// 使用指定路径快速给成员对象赋值
|
||||
OmniAccessor.set(parent, "child/grandChild/content", "demo child")
|
||||
Assertions.assertEquals("demo child", parent.child?.grandChild?.content)
|
||||
assertEquals("demo child", parent.child?.grandChild?.content)
|
||||
|
||||
// 使用带下标的路径给成员对象赋值
|
||||
OmniAccessor.set(parent, "children[1]/grandChild/content", "demo children[1]")
|
||||
Assertions.assertEquals("demo children[1]", parent.children?.get(1)?.grandChild?.content)
|
||||
assertEquals("demo children[1]", parent.children?.get(1)?.grandChild?.content)
|
||||
|
||||
// 使用模糊路径批量给成员对象赋值
|
||||
OmniAccessor.set(parent, "child*/*/content", "demo in batch")
|
||||
Assertions.assertEquals("demo in batch", parent.child?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(0)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(1)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(2)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.child?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(0)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(1)?.grandChild?.content)
|
||||
assertEquals("demo in batch", parent.children?.get(2)?.grandChild?.content)
|
||||
|
||||
// 读写私有内部类类型的成员(使用类型名引用内部类时,无需带外部类名)
|
||||
Assertions.assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
OmniAccessor.set(parent, "{InnerChild}/secret", "inner-class secret")
|
||||
Assertions.assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -15,31 +15,31 @@ internal class DemoPrivateAccessorTest {
|
||||
@Test
|
||||
fun should_access_private_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
assertEquals("abc + hello + 1", invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3)
|
||||
assertEquals(3, PrivateAccessor.get(demoPrivateAccess, "count"))
|
||||
set(demoPrivateAccess, "count", 3)
|
||||
assertEquals(3, get(demoPrivateAccess, "count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_static_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
assertEquals("hello + 1", invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_access_private_static_field() {
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
|
||||
assertEquals(3, PrivateAccessor.getStatic(DemoPrivateAccess::class.java, "staticCount"))
|
||||
setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
|
||||
assertEquals(3, getStatic(DemoPrivateAccess::class.java, "staticCount"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_update_final_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 4.13)
|
||||
set(demoPrivateAccess, "pi", 4.13)
|
||||
assertEquals(4.13, demoPrivateAccess.pi)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
@@ -42,22 +41,22 @@ internal class DemoTemplateTest {
|
||||
@Test
|
||||
fun should_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
Assertions.assertEquals("demo_mock_list", res)
|
||||
assertEquals("demo_mock_list", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_double_template_method() {
|
||||
val res = demoTemplate.doubleTemplateMethod()
|
||||
Assertions.assertEquals("testable_mock_map", res)
|
||||
assertEquals("testable_mock_map", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_new_template_method() {
|
||||
val res = demoTemplate.newTemplateMethod()
|
||||
Assertions.assertEquals(2, res.size)
|
||||
assertEquals(2, res.size)
|
||||
val iterator = res.stream().iterator()
|
||||
Assertions.assertEquals("insert_mock", iterator.next())
|
||||
Assertions.assertEquals("world_mocked", iterator.next())
|
||||
assertEquals("insert_mock", iterator.next())
|
||||
assertEquals("world_mocked", iterator.next())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@MockWith
|
||||
@@ -14,9 +14,9 @@ class OneToMultiSvcTest {
|
||||
|
||||
@Test
|
||||
fun should_test_multi_class_together() {
|
||||
Assertions.assertEquals("a_mock", aSvc.demo("test"))
|
||||
Assertions.assertEquals("b_mock", bSvc.demo("test"))
|
||||
Assertions.assertEquals("c_mock", cSvc.demo("test"))
|
||||
assertEquals("a_mock", aSvc.demo("test"))
|
||||
assertEquals("b_mock", bSvc.demo("test"))
|
||||
assertEquals("c_mock", cSvc.demo("test"))
|
||||
verify("a_format").withTimes(1)
|
||||
verify("b_format").withTimes(1)
|
||||
verify("c_format").withTimes(1)
|
||||
|
||||
2
demo/kotlin-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
omni.constructor.enhance.enable = true
|
||||
mock.target.checking.enable = true
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
rootProject.name = 'demos'
|
||||
|
||||
include 'java-demo'
|
||||
include 'kotlin-demo'
|
||||
include 'spock-demo'
|
||||
28
demo/spock-demo/build.gradle
Normal 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.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.9')
|
||||
}
|
||||
|
||||
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
@@ -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.9</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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,4 +10,4 @@ However, when the current mainstream mock framework implements the mock function
|
||||
Therefore, we developed `TestableMock`, **a maverick and lightweight mock tool**.
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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 😃
|
||||
|
||||
@@ -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,26 +37,28 @@ 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`.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.9
|
||||
- support mock invocation in lambda method or via function reference
|
||||
- fix a `NullPointerException` issue when `PrivateAccessor.invoke()` has `null` parameter
|
||||
|
||||
## 0.6.8
|
||||
- support `@DumpTo` annotation to dump bytecode of any transformed class
|
||||
- `PrivateAccessor.setStatic()` method now able to update static final members
|
||||
|
||||
## 0.6.7
|
||||
- support empty mock container class which has all mock methods in its parent
|
||||
- fix an issue cause mock fail when parameter has array element accessing
|
||||
|
||||
## 0.6.6
|
||||
- support global package path mapping for easier mocking code of 3rd packages
|
||||
- complete mock target checking, improve resistance to code refactoring
|
||||
- fix a log disorder issue of mock class handler
|
||||
- fix an error of creating `Long` or `Integer` class with `OmniConstructor` in certain jvm version
|
||||
|
||||
## 0.6.5
|
||||
- fix an issue cause `OmniConstructor` fail to handle classes like Date and BigDecimal
|
||||
- fix an issue of `PrivateAccessor` fail to invoke method with single array parameter
|
||||
- fix an issue of mock methods in super class are ignored when `scope` is `associated`
|
||||
- fix an issue of empty mock method not works when `scope` is `associated`
|
||||
|
||||
## 0.6.4
|
||||
- `TestableNull` class removed, let `OmniConstructor` even lighter
|
||||
- support `@Nested` annotation of JUnit 5
|
||||
- fix several `OmniConstructor` compatibility issues(thanks to [@ddatsh](https://github.com/ddatsh))
|
||||
|
||||
## 0.6.3
|
||||
- support custom inner `Mock` class name
|
||||
- support skip specified packages from `OmniConstructor` enhancement
|
||||
- fix 2 circle construction cases in `OmniConstructor`
|
||||
- fix an index out-of-range issue caused by non-static method without `this` reference
|
||||
- fix a null pointer exception caused by resource folder unavailable at runtime
|
||||
|
||||
## 0.6.2
|
||||
- support setup global testable agent configure via properties file
|
||||
- disable OmniConstructor bytecode enhancement by default
|
||||
- fix a `Spring` framework compatibility issue with OmniConstructor
|
||||
|
||||
## 0.6.1
|
||||
- generate mock scanning log file automatically, for self-troubleshooting
|
||||
- fix a `Spock` test 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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
|
||||
## Use in Maven project
|
||||
|
||||
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
<testable.version>0.6.9</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.9')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ test {
|
||||
|
||||
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
|
||||
|
||||
> For Android project tested with `Robolectric` framework, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
> For Android project, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
@@ -92,4 +92,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -15,4 +15,4 @@ At the same time, because the built-in unit test executor of `Eclipse` completel
|
||||
|
||||
Take the use of `JUnit` as an example. You need to pull down from the small triangle next to the run button on the IDE toolbar, select "Run Configurations...", select the task to run the unit test on the left side, and switch to "arguments" Tab on the right side, append a `-javaagent:` parameter in the "VM Options", the following figure is an example, note that the `testable-agent` package should be modified to match the actual situation of the local Maven repository path.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -29,13 +29,13 @@ This is because the IDE usually only runs the `maven-surefire-plugin` plugin whe
|
||||
|
||||
This problem can be bypassed by configuring the test parameters of the IDE additionally. Take IntelliJ as an example, open the "Edit Configuration..." option of the run menu, as shown in the position ①
|
||||
|
||||

|
||||

|
||||
|
||||
Add JavaAgent startup parameters at the end of the "virtual machine parameters" attribute value: `-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/xyz/testable-agent-xyzjar`, as shown in the figure position ②
|
||||
|
||||
> PS: Please replace `x.y.z` in the path with the actual version number
|
||||
|
||||

|
||||

|
||||
|
||||
Finally, click to run the unit test, as shown in the position ③
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
4
docs/en-us/doc/use-package-mapping.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Use Package Mapping
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
4
docs/en-us/doc/with-thread-pool.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Mock Invocation In Thread Pool
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -7,19 +7,21 @@
|
||||
|
||||
- Usage Guide
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
|
||||
- [Effective Scope Of Mocking](zh-cn/doc/scope-of-mock.md)
|
||||
- [Reusing Mock Methods](zh-cn/doc/mock-method-reusing.md)
|
||||
- [Use MockWith Annotation](zh-cn/doc/use-mock-with.md)
|
||||
- [Effective Scope Of Mocking](en-us/doc/scope-of-mock.md)
|
||||
- [Reusing Mock Methods](en-us/doc/mock-method-reusing.md)
|
||||
- [Use MockWith Annotation](en-us/doc/use-mock-with.md)
|
||||
- [Use Package Mapping](en-us/doc/use-package-mapping.md)
|
||||
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
|
||||
- [Mock Invocation In Thread Pool](zh-cn/doc/with-thread-pool.md)
|
||||
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
|
||||
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Upgrade Guide](en-us/doc/upgrade-guide.md)
|
||||
- [Available Global Arguments](zh-cn/doc/javaagent-args.md)
|
||||
- [Available Annotations](zh-cn/doc/annotations.md)
|
||||
- [How Mock Works](zh-cn/doc/design-and-mechanism.md)
|
||||
- [Available Global Arguments](en-us/doc/javaagent-args.md)
|
||||
- [Available Annotations](en-us/doc/annotations.md)
|
||||
- [How Mock Works](en-us/doc/design-and-mechanism.md)
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [About Us](en-us/doc/about-us.md)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
TestableMock简介
|
||||
---
|
||||
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。某个调用需要被Mock,往往只与其自身特征有关,而与调用的来源无关。
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。
|
||||
|
||||
在定义Mock方法时,开发者真正关心的只有一件事:"<u>这个调用,在测试的时候要换成那个假的Mock方法</u>"。
|
||||
|
||||
@@ -9,4 +9,4 @@ TestableMock简介
|
||||
|
||||
于是,我们开发了`TestableMock`,**一款特立独行的轻量Mock工具**。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -58,3 +58,12 @@
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
|
||||
|
||||
#### @DumpTo
|
||||
|
||||
将指定类型被`TestableMock`处理过后的字节码导出到文件。
|
||||
|
||||
- 作用于:任意类型
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | String | 是 | N/A | 存放导出文件的目录(相对当前执行目录的相对路径),若目录不存在,将自动创建 |
|
||||
|
||||
@@ -8,9 +8,9 @@ TestableMock的设计和原理
|
||||
这种设计基于两项基本假设:
|
||||
|
||||
1. 同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
|
||||
2. 需要Mock的调用都来自被测类的代码。(此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock)
|
||||
2. 每个单元测试只关注被测单元内部的逻辑,单元外的无关调用应该被替换为Mock。即需要被Mock的调用应该都在被测类的代码中。
|
||||
|
||||
据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。
|
||||
据此通过约定来简化符合以上假设的单元测试场景,通过配置来支持其余更复杂的使用场景。
|
||||
|
||||
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码,把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
问题反馈
|
||||
---
|
||||
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在24小时内回复。
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在每周末之前统一回复处理。
|
||||
|
||||
请详细描述遇到的情况,若您觉得问题可能与`TestableMock`的潜在BUG有关,请尽量提供可复现的相关源码或示例,以便进行针对性定位和排查。
|
||||
|
||||
|
||||
@@ -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,26 +41,26 @@
|
||||
|
||||
> 由于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`。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,45 +1,62 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过全局配置文件调整其执行过程。
|
||||
|
||||
### 可用参数清单
|
||||
## 1.使用全局配置文件
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
在项目的`src/test/resources`目录下创建一个名为`testable.properties`的文本文件,其中每行是一条配置项,配置项的名称与值之间用`=`连接。
|
||||
|
||||
详细的可用配置项列说明下:
|
||||
|
||||
| 配置项 | 描述 | 可用值和示例 |
|
||||
| ---- | ---- | ---- |
|
||||
| dump.path | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
|
||||
| enhance.pkgPrefix.includes | 让`TestableMock`仅在特定包内生效,通常用于大型项目 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.biz,com.3rd.biz` |
|
||||
| log.file | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
|
||||
| log.level | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| mock.innerClass.name | 修改测试类中的内置Mock容器类名 | 任意符合Java类名的值,默认值为`Mock` |
|
||||
| mock.scope.default | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| mock.target.checking.enable | 是否启用Mock目标有效性的前置检查 | 可用值为:`true` / `false`,当前默认值为`false` |
|
||||
| omni.constructor.enhance.enable | 是否启用`OmniConstructor`的字节码增强模式 | 可用值为:`true` / `false` |
|
||||
| omni.constructor.enhance.pkgPrefix.excludes | 对特定包禁用`OmniConstructor`的字节码增强模式 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.model` |
|
||||
| private.access.enhance.enable | 是否启用`PrivateAccessor`的字节码增强模式 | 可用值为:`true` / `false` |
|
||||
| thread.pool.enhance.enable | 是否启用基于`TransmittableThreadLocal`的Mock上下文存储 | 可用值为:`true` / `false` |
|
||||
|
||||
参见`demo`目录各示例项目中的`testable.properties`文件。
|
||||
|
||||
## 2.修改配置文件位置
|
||||
|
||||
除了配置文件,`TestableMock`也支持通过引入`testable-agent`包时,在末尾加上一个`=`符号,然后连接额外参数来实现全局配置。这种方法与使用配置文件基本等效,因此通常无需使用,但有唯一特例是`configFile`配置项,该配置项可用于修改全局配置文件的位置。
|
||||
|
||||
| 参数 | 描述 | 可用值和示例 |
|
||||
| ---- | ---- | ---- |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| 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&pkgPrefix=com.demo.</argLine>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=configFile=config/testable.properties</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&`
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=configFile=config/testable.properties"
|
||||
```
|
||||
|
||||
## 3. 全局配置技巧
|
||||
|
||||
对于一般的小型项目,`TestableMock`的所有功能都是开箱即用的,正确引用依赖包后,无需进行额外配置。
|
||||
|
||||
默认情况下,`TestableMock`会在构建目录中(Maven构建的`target`目录或Gradle构建的`build`目录)自动生成一个记录执行过程的`testable-agent.log`文件。若希望禁用此日志文件,可将`log.file`参数赋值为`null`。
|
||||
|
||||
若测试中使用了`OmniConstructor`且遇到构造出错的情况,可开启`omni.constructor.enhance.enable`配置(倘若开启后依然报错,请提Issue告诉我们)。`omni.constructor.enhance.pkgPrefix.excludes`配置主要用于当开启`OmniConstructor`字节码增强模式报错时,临时绕过某些无法处理的类型,通常无需使用。
|
||||
|
||||
若项目较大(构建生成的jar包大于100MB),可考虑使用`enhance.pkgPrefix.includes`参数来减少`TestableMock`在测试启动前建立Mock关联和进行`OmniConstructor`预处理的扫描时长,从而加快单元测试启动速度。通常将值设置为当前项目自身的`<group>.<artifact>`路径即可,如需Mock三方包中的调用,或通过`OmniConstructor`构造来自三方包中的类型,则还应该加上相应的三方包路径。
|
||||
|
||||
若项目测试中,既包含真实的单元测试,又包含了使用单元测试框架编写的集成测试时。为了让集成测试的执行过程不受Mock影响,可能需要使用`mock.scope.default`将默认的Mock方法范围限制为仅对所属类型的单元测试用例生效。
|
||||
|
||||
若需Mock的调用发生在线程池中,且遇到`verify()`结果或`MOCK_CONTEXT`内容不正确的时候,则需考虑开启`thread.pool.enhance.enable`配置,详见[Mock线程池内的调用](zh-cn/doc/with-thread-pool.md)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
复用Mock类与方法
|
||||
---
|
||||
|
||||
Don't Repeat Yourself是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
“Don't Repeat Yourself”是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
|
||||
TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>复用Mock方法</u>。
|
||||
|
||||
@@ -11,7 +11,6 @@ TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>
|
||||
|
||||
进行类级别的Mock复用,只需将Mock容器定义为独立的类,然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
|
||||
|
||||
|
||||
```java
|
||||
@MockWith(ServiceMock.class)
|
||||
public class AaaServiceTest {
|
||||
|
||||
@@ -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`方式来读写业务类的成员字段(虽然技术上可行)。
|
||||
|
||||