前言

测试代码的写法可以归纳为三部分:准备测试数据和定义mock行为、调用真实的函数以及调用验证函数进行结果的验证。在Junit4中,我们可以在模块的test路径下编写测试案例。在类中使用@Test注解,就可以告诉Junit这个方法是测试方式。同时使用assert*方法,可以调用Junit进行结果的验证。

Junit4常用注解

除了@Test注解,还有以下常见的注解可供使用:

1. @BeforeClass:会在所有的方法执行前被执行,static 方法 (全局只会执行一次,而且是第一个运行)。全局的含义是在一个测试类中。

2. @AfterClass:会在所有的方法执行之后进行执行,static 方法 (全局只会执行一次,而且是最后一个运行)。

3. @Before:会在每一个测试方法被运行前执行一次,可以用来清理执行环境,保证测试用例在执行前具有干净的上下文。

4. @After:会在每一个测试方法运行后被执行一次。

可选使用Junit的Rule简化代码

和Junit的@Before和@After分别作用于每一个单元测试案例的开始和结束类似,@Rule注解提供了同样的能力,但有一个好处就是执行前,执行单元测试和执行后在同一个方法中,包含在同一个上下文中,这能让我们更加灵活的处理单元测试。使用起来也比较简单:

第一步:实现TestRule接口

```java

import org.junit.rules.TestRule;

import org.junit.runners.model.Statement;

public class MyRule implements TestRule {

@Override

public Statement apply(Statement base, Description description) {

return new Statement() {

@Override

public void evaluate() throws Throwable {

// 在每个测试方法执行前的逻辑处理

System.out.println("Before each test method");

base.evaluate();

// 在每个测试方法执行后的逻辑处理

System.out.println("After each test method");

}

};

}

}

```

public class MethodNameExample implements TestRule {

@Override

public Statement apply(Statement base, Description description) {

// 在测试方法运行之前做一些事情,就在base.evaluate()之前做

String className = description.getClassName();

String methodName = description.getMethodName();

base.evaluate(); // 这其实就是运行测试方法

// 在测试方法运行之后做一些事情,就在base.evaluate()之后做

System.out.println("Class name: " + className + ", method name: " + methodName);

return base;

}

}

第二步:在Test类中使用。加上@Rule注解即可

```java

@Rule

public MethodNameExample methodNameExample = new MethodNameExample();

```

使用Parameterized特性减少重复测试用例(Junit5自带,Junit4需额外引入依赖)

根据不同的输入,待测试函数会有不同的输出结果,那么我们就需要针对每一类的输入,编写一个测试用例,这样才能覆盖待测函数的所有逻辑分支。(写多少个测试用例能够覆盖全所有的逻辑分支可称之为待测函数的圈复杂度)。

使用Junit4提供的Parameterized Tests特性,可以帮助我们减少用例编写的代码,使测试类更清晰简单,而且数据可以从CSV文件导入。以下提供一个例子:

第一步:引入依赖

```java

testImplementation(rootProject.ext.dependencies.jupiter)

```

第二步:测试类中添加注解

在Junit4中,我们可以使用@RunWith(Parameterized.class)注解来实现参数化测试。参数化测试可以使得测试用例更加灵活,只需要提供输入参数和预期输出结果,就可以自动生成多个测试用例。具体步骤如下:

1. 使用@RunWith(Parameterized.class)注解标注测试类;

2. 定义实例变量,明确输入和输出。例如,定义2个变量,一个是预期的输出结果,一个是输入的参数;

3. 定义构造函数,在构造函数中对变量赋值;

4. 定义数据集,使用注解标注,返回一个数组,数组代表的就是Junit4需要提供给构造函数进行实例化的数据集;

5. 编写测试用例。对于以下的测试用例,Junit4会使用第五步的数据进行填充,执行3次。

在这个例子中,我们使用了Mockito框架来进行模拟对象的创建。Mockito可以根据需要mock出虚假的对象,在测试环境中,可以用来替换掉真实的最像,达到两大目的:验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等;指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作。

Mockito是一个Java测试框架,它可以帮助我们模拟对象的行为。在Mockito中,我们可以使用mock()、spy()和verify()等方法来模拟对象的行为。下面是一些关于如何使用Mockito模拟对象行为的示例:

1. 使用mock()方法模拟对象

```java

// 使用mock函数即可模拟一个对象,这个对象的实例变量均为默认值

BioGuidePresenter presenter = Mockito.mock(BioGuidePresenter.class);

// 设定这个mock对象被调用某一方法时,应该返回的结果

Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

```

2. 使用spy()方法模拟对象

```java

// 使用spy函数即可模拟一个对象,这个对象的实例变量均为默认值

BioGuidePresenter presenter = Mockito.spy(BioGuidePresenter.class);

// 设定这个mock对象被调用某一方法时,应该返回的结果

Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

```

spy()和mock()的区别在于,未指定mock方法的返回值时,默认返回null,而为指定spy方法的返回值时,默认执行目标方法的逻辑,并返回对应逻辑执行的结果。另外有一个很重要的区别在于,使用spy的情况下,虽然提供了函数的模拟实现,但Mockito框架仍然会调用真实的代码,所以如果真实代码无法在单测下运行,则使用spy模拟会导致测试失败。

3. 使用Mockito验证结果

```java

// 验证mock的database对象,调用setUniqueId方法时的入参是否为12

verify(database).setUniqueId(ArgumentMatchers.eq(12));

// 验证mock的database对象的getUniqueId方法是否被调用2次

verify(database, times(2)).getUniqueId();

// 验证mock的database对象的getUniqueId方法是否被调用1次

verify(database).getUniqueId();

// 也可以使用传统的Junit判断方法判断结果是否符合预期

assertEquals("foo", spy.get(0));

```

Mockito版本升级后,支持对静态方法进行Hook。在build.gradle中引入Mockito-inline依赖即可实现这一功能。以下是一个实例代码,展示了如何在测试类中调用doSomethings()方法时,通过Hook DataEngine或MemoryService来获取期望的返回值。

```java

public void doSomethings() {

DataEngine.getMemoryService().saveCacheObject("key", "abc");

...

String a = DataEngine.getMemoryService().getCacheObject("key");

doSomething2(a);

}

```

接下来,我们将介绍如何使用mockito-inline对静态方法进行处理。

一、Hook静态方法:

1. 使用Java7的try-with-resource语法模拟触发静态方法(DataEngine.getMemoryService)的行为。需要注意的是,mockService可以是通过Mockito mock出来的,也可以是我们创建的一个真实的MemoryService子类。区别在于,使用Mockito mock的MemoryService我们不需要实现所有的方法,只需要mock我们测试类中可能调用到的方法。

```java

MemoryStoreService mockService = Mockito.mock(MemoryStoreService.class);

try (MockedStatic service = Mockito.mockStatic(DataEngine.class)) {

service.when(DataEngine::getMemoryService).thenReturn(mockService);

}

```

二、使用更加智能的模拟返回方法:

在上述示例中,我们已经使用了try-with-resource语法和Mockito.mockStatic()方法来Hook静态方法。现在,我们可以使用Mockito的更多高级功能来模拟返回值。例如,我们可以使用Mockito.when()方法来指定当调用某个静态方法时,返回特定的值。

```java

service.when(DataEngine::getMemoryService).thenAnswer(invocation -> {

MemoryStoreService result = invocation.getArgument(0);

result.saveCacheObject("newKey", "newValue");

return result;

});

```

这样,当我们调用DataEngine.getMemoryService()时,它将返回一个已经被修改过的MemoryStoreService实例,其中包含了我们指定的缓存对象。

在处理模拟的入参时,我们通常使用`thenReturn()`方法。然而,在本案例的场景下,我们需要一个功能更强大的返回方法。这是因为:

1. `MemoryService::saveCacheObject`的返回值是`Void`,所以无法使用`thenReturn()`方法。

2. 我们需要处理入参,针对每一个`saveCacheObject`的模拟调用,我们都需要真实地将其保存到`Map`中。

为了实现这个需求,我们可以使用`doAnswer()`方法来模拟`saveCacheObject`方法。首先,我们需要创建一个`Map`对象来存储键值对:

```java

final Map pools = new HashMap<>();

```

接下来,我们使用`doAnswer()`方法创建一个新的`Answer`对象,并重写其`answer()`方法。在这个方法中,我们将传入的参数(Key和Value)添加到`pools`映射中,并返回`null`:

```java

Mockito.doAnswer(new Answer() {

@Override

public Object answer(InvocationOnMock invocation) throws Throwable {

pools.put((String) invocation.getArgument(0), invocation.getArgument(1));

return null;

}

}).when(mockService).saveCacheObject(Mockito.anyString(), Mockito.any());

```

当我们使用`doAnswer`模拟了`saveCacheObject`方法后,我们可能还需要使用相同的策略来模拟`getCacheObject`方法。例如:

```java

Mockito.when(mockService.containsCachedObject(Mockito.anyString()))

.thenAnswer(invocation -> pools.containsKey(invocation.getArgument(0)));

```

最后,如果需要测试一段异步代码,可以使用标准的异步代码测试步骤进行。例如:

```java

import org.junit.jupiter.api.Test;

import org.mockito.Mockito;

import static org.mockito.ArgumentMatchers.anyList;

import static org.mockito.Mockito.doNothing;

import static org.mockito.Mockito.verify;

public class DemoServiceTest {

private final DemoService demoService = new DemoService();

private final Repo repo = Mockito.mock(Repo.class);

@Test

public void testUpdate() {

List demos = new ArrayList<>();

// 模拟异步回调函数

Repo.OnRefreshListener mockListener = Mockito.mock(Repo.OnRefreshListener.class);

doNothing().when(repo).refresh(demos, mockListener);

// 主动调用异步回调接口,使执行流进入回调函数中

demoService.update(demos, repo);

// 判断是否执行了doSomething()方法,或者执行结果是否符合预期的其他判断方式

verify(repo).refresh(anyList(), mockListener);

Mockito.verify(mockListener).onResult();

}

}

```

```java

public void demo(String id) {

RpcService.send(new DemoReq(id), new RpcCallback() {

@Override

public void onFailure(BaseReq call, String msg, String procCd, String procSts, Exception e) {

if (listener != null) {

listener.onFailure(msg, procCd, procSts);

}

}

@Override

public void onResponse(BaseReq call, WalletDetailRespMsg response) {

if (listener != null) {

listener.onSuccess(response);

}

}

});

}

```

```kotlin

import org.junit.jupiter.api.Assertions.*

import org.mockito.Mockito

import org.mockito.stubbing.Answer

import java.util.concurrent.atomic.AtomicBoolean

class RpcService {

fun send(param1: Any, param2: RpcCallback) {

// ...

}

}

interface RpcCallback {

fun onResponse(baseReq: BaseReq, walletDetailRespMsg: WalletDetailRespMsg)

}

class WalletDetailRespMsg {

// ...

}

class BaseReq {

// ...

}

class TestClass {

private val testWalletId = "test_wallet_id"

private val callback: RpcCallback by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {

val mockCallback = Mockito.mock(RpcCallback::class.java)

Mockito.doAnswer(answer =: { invocation ->

val baseReq = invocation.getArgument(0)

val walletDetailRespMsg = invocation.getArgument(1)

mockCallback.onResponse(baseReq, walletDetailRespMsg)

null

}).when(mockCallback).onResponse(any(), any())

return mockCallback

}

@Test

fun testRefreshWalletDetail() = runBlockingTest {

val presenter = Mockito.mock(Presenter::class.java)

Mockito.doNothing().when(presenter).refreshWalletDetail(testWalletId, callback)

presenter.refreshWalletDetail(testWalletId, callback)

verify(callback).onSuccess(Mockito.any())

}

}

```

为了简化测试代码,我们可以使用Roboletric库来模拟Activity页面。首先,我们需要在测试类中添加以下依赖:

```groovy

testImplementation 'org.robolectric.android.testing:robolectric:4.6.1'

testImplementation 'org.jetbrains.kotlinx:kotlinx-test-junit-runner:1.3.2'

```

然后,我们可以创建一个名为`getAppMock`的函数,该函数接受一个高阶函数作为参数,并使用Roboletric模拟`Apps.getApp()`方法的返回结果。这样,我们可以在测试中轻松地进行串联调用。

```kotlin

import org.jetbrains.annotations.NotNull

import org.mockito.Mockito

import org.robolectric.Robolectric

import org.robolectric.shadows.ShadowActivity

fun getAppMock(@NotNull action: () -> Any?) {

val activity = Robolectric.setupActivity(Any()) // 替换为你需要模拟的Activity类

Mockito.mockStatic(Apps::class.java).use { appsMock ->

appsMock.`when` { Apps.getApp() }.thenReturn(activity)

action()

}

}

```

现在,我们可以在测试方法中使用`getAppMock`函数来模拟Activity页面。例如,对于上面的`reduceWithUserRejectTest()`方法,我们可以将其重构为:

```kotlin

import org.junit.Test

import org.mockito.ArgumentCaptor

import org.robolectric.Robolectric

import org.robolectric.shadows.ShadowActivity

class PaymentPageStateTest {

@Test

fun reduceWithUserRejectTest() {

val change = HceDefaultChange(true)

getAppMock { isNfcDefaultPaymentMockStatic(true) { checkNetMockStatic(true) {

val actual: PaymentPageState = change.reduce(PaymentPageState())

Assert.assertTrue(actual.showWaving)

} } }

}

}

```

这样,我们就可以使用Roboletric库来简化测试代码,使其更接近Flutter或Compose的UI页面。

首先,我们需要在测试类中添加`RobolectricTestRunner`,作为运行Roboletric的启动器。接着,我们需要使用`Config`配置本次单元测试的基础配置。具体包括:

1. 如果电脑上运行的JAVA版本不是11以上,则需要指定SDK版本为Android 9.0以下。

2. 可以指定shadows。shadows下文会详细解析,这里可配置可不配置,取决于具体场景。

3. qualifiers可以配置机器的尺寸,多语言环境等,可配置可不配置,取决于具体场景。例子中指定了中文环境。

```java

@RunWith(RobolectricTestRunner.class)

@Config(sdk = {Build.VERSION_CODES.O_MR1}, shadows = {DemoShadow.class}, qualifiers = "zh")

public class DemoTest {

}

```

接下来,我们可以使用Roboletric模拟Activity。Roboletric的一大特点就是可以模拟Android的context。我们可以在`@Before`注解的方法中使用Roboletric创建一个Activity。

```java

@Before

public void initActivity() {

// Intent可选

Intent faceIntent = new Intent();

faceIntent.putExtra(DEMO, uri.toString());

activity = Robolectric.buildActivity(VerificationBioGuideActivity.class, faceIntent)

.create().resume().get();

}

```

通过调用`buildActivity`方法,我们可以模拟一个Activity。调用`create`方法可以触发`onCreate`回调,调用`resume`方法可以触发`onResume`回调,最后调用`get`方法就可以拿到这个activity对象。拿到activity的对象之后,我们就可以通过activity进行一些操作了。例如,获取View的控件,获取字符串等。

以下是重构后的代码:

```java

// 获取View控件

TitleBar titleBar = (TitleBar) activity.findViewById(R.id.title_bar);

// 获取字符串

String verificationBioPayTitleFingerSuccessTips = activity.getString(R.string.verification_bio_pay_title_finger_success_tips);

// 使用Roboletric模拟出来的activity作为context,如果只需要用到applicationContext,可以使用RuntimeEnvironment.getApplication()

// Roboletric的杀手锏——Shadows

// Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。

// Shadows的作用就是使用自定义的方法和类替换原先业务的方法和类,原理就是使用字节码修改技术进行动态的修改。例如,业务中A.class原先要调用B.class的C()函数,我们使用Shadows,并定义一个函数签名一样的函数D()将其作用于C()函数上。当触发A.class的调用后,程序执行流程会进入D()函数中。

// 自定义Shadows简介

// 第一步: 使用@Implements类定义需要被替换的目标类。Shadows类本身也是一个普通的Java类,只不过多了@Implements注解,所以他也可以实现接口,继承等功能。

// 表明这个Shadow期望模拟ActualClass类的行为 @Implements(ActualClass.class) @RequiresApi(api = Build.VERSION_CODES.N) public class ActualClassShadow { }

// 第二步: 与目标函数使用相同的函数签名,并且增加@Implementation注解。可以只替换目标类中的部分函数,而其他函数仍然遵循目标类的逻辑。

// 表明需要模拟ActualClass中的containsKey方法 @Implementation public boolean containsKey(String key) { }

// 第三步(可选):__constructor__可以替换构造函数

```

以下是根据您提供的内容重构的代码,并添加了相应的注释:

```java

public class ActualClass {

private Point realPoint; // 目标对象

private int x;

private int y;

public void __constructor__(int x, int y) {

this.x = x;

this.y = y;

}

@Override

public String toString() {

return "ActualClass(x=" + x + ", y=" + y + ")";

}

// 其他方法...

}

```

使用Chat-GPT生成单元测试案例的步骤如下:

1. 让Chat-GPT知晓该函数的意图。告诉它我们需要为`ActualClass`类编写一个单元测试案例。

2. 告诉Chat-GPT单元测试的目标。确保Chat-GPT知道我们要测试的是`ActualClass`类的哪些方面。

3. (可选)可以指定Chat-GPT使用Junit4的一些测试特性简化单测案例。例如,我们可以使用`ParameterizedJUnit`,以便在不同的参数组合下运行测试。

4. 在代码中,我们使用了`@Config(shadows = {ActualClassShadow.class})`注解,这样我们就可以使用`RealObject`来获取目标对象,并对其进行配置。

5. 最后,在实际代码中,我们还需要定义好Shadows之后,我们需要再测试类的Config注解中进行注册。