0%

TestNG Jmockit 使用案例

记录一下工作中常用到的 TestNG, Jmockit 使用案例

DataProvider

单个参数

1
2
3
4
5
6
7
8
9
10
11
12
@DataProvider(name = "singleParam")
public Object[][] singleParam() {
return new Object[][]{
{"Jerry"},
{"Tom"},
};
}

@Test(dataProvider = "singleParam")
public void single_data(String username) {
System.out.println("Get username: " + username);
}

idea 中通过设置 live template 简化操作

  • cmd + , 调出设置界面,搜索 live template
  • 点击 +, 添加 group, 命名为 Unit Test
  • 选中 group,点击 + 添加新规则
  • 模版中输入下面的模版案例
  • 点击 Context 的 define, 选中 java -> declear
  • 点击 Edit variables, Expression 中输入默认值,比如 “methodName”, 这里的规则比较绕,试了好久,至少能 work
1
2
3
4
5
6
@org.testng.annotations.DataProvider(name = "$DATA_PROVIDER_NAME$")
public Object[][] $METHOD_NAME$() {
return new Object[][]{
{$OBJECT$}
};
}

多个参数

1
2
3
4
5
6
7
8
9
10
11
12
@DataProvider(name = "multiParam")
public Object[][] multiParam() {
return new Object[][]{
{"Jerry", 12},
{"Tom", 11},
};
}

@Test(dataProvider = "multiParam")
public void single_data(String username, int age) {
System.out.println("Get username: " + username + ", age: " + age);
}

Mock 类的静态代码块

测试类结构如下

1
2
3
4
5
6
7
public class ClientIPUtils {
private static String token = null;

static {
token = someService.getToken();
}
}

这种类型的测试中,可以通过以下方式绕过 静态代码块 中的逻辑。

1
2
3
4
5
6
7
8
@BeforeClass
public static void before() {
new MockUp<VaultUtil>() {
@Mock
void $clinit() {
}
};
}

如果你的测试逻辑需要不同的 token,你不应该在 case level mock 他,因为它是类级别的代码,jvm 启动的时候只执行一次,之前我像下面这样写测试,导致第二个测试一直失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test1() {
new Expectations() {
{
someService.getToken();
result = "fake";
}
};
}

@Test
public void test2() {
new Expectations() {
{
someService.getToken();
result = "fake";
}
};
}

解决办法是,通过 MockUp 绕过静态代码块的初始化,当需要改变值的时候,通过 Deencapsulation.setField(Class, field_name, field_value); 实现

Mocked 作用域

如果是 global 参数,那么所有 class 内的 case 都会有影响,如果是 method level 的那只有对应的 case 有影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Teacher{
public String name = "unnamed";

public Teacher(String name) {
this.name = name;
}
}

@Mocked Teacher teacher;
@Test
public void test() { System.out.println(teacher.name); } //output: null

@Test
public void test02 () { System.out.println(new Teacher("Jack").name); } //output: null

如果做 method level 的 mock, 只作用 case 本身

1
2
3
4
5
@Test
public void test(@Mocked Teacher teacher) { System.out.println(teacher.name); } // null

@Test
public void test02 () { System.out.println(new Teacher("Jack").name); } // Jack

Jmockit 和 TestNG 兼容性问题

TestNG 6.9.11+ 和 Jmockit 有兼容性问题,将 @Mocked 通过参数方式传入会抛 Exception

1
2
3
4
5
6
7
8
9
10
public class CompatibleTest {
@Test
public void test(@Mocked UserBean userBean) {}
}

//output:
// org.testng.internal.reflect.MethodMatcherException:
// Data provider mismatch
// Method: test([Parameter{index=0, type=com.objects.UserBean, declaredAnnotations=[@mockit.Mocked(stubOutClassInitialization=false)]}])
// Arguments: []

修复方法:将 @Mocked 部分提取改为 global 的变量即可

1
2
3
4
5
6
public class CompatibleTest {
@Mocked UserBean userBean;

@Test
public void test() {}
}

如果我还想保留这种 case level 的使用,需要做点什么?这种 case level 的使用在作用域控制上更好

TODO

Mock 不带默认构造函数的对象

构建一个测试对象时,如果他没有默认构造函数的话需要为参数声明 @Injectable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 测试对象
class Dog{
private String name;

public Dog(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

public class CompatibleTest {
@Tested Dog dog;
@Injectable String name;

@Test
public void test() { dog.getName(); }
}

// 如果没加的话抛出异常
// java.lang.IllegalArgumentException: No constructor in tested class that can be satisfied by available injectables
// public com.successfactors.legacy.service.provisioning.impl.Dog(String)
// disregarded because no injectable was found for parameter "name"

Mockup 工厂方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* new object + mockup, new object 发生在 mock 之后,所以 mock 生效
*/
@Test
public void mock_factory_using_mockup() {
new MockUp<NPCFactory>() {
@Mock
public Person getNPC() {
return new Person("mock", 1);
}
};

ClassRoom classRoom = new ClassRoom();
assertEquals("mock", classRoom.getNPCName());
}

public class ClassRoom {
private Person npc = NPCFactory.getNPC();
public String getNPCName() { return npc.getName(); }
}

使用 Deencapsulation 设置私有变量,高版本已经 deprecated

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* new object + expectations, new object 发生在 mock 之后,所以 mock 生效
*/
@Test
public void mock_factory_using_deencapsulation(@Mocked final Person person) {
new Expectations() {{
person.getName();
result = "deenMock";
}};

Deencapsulation.setField(room, "npc", person);
assertEquals("deenMock", room.getNPCName());
}

通过 Expectations case level mock 静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* new object + expectations, new object 发生在 mock 之后,所以 mock 生效
*/
@Test
public void mock_factory_using_expectations() {
new Expectations(NPCFactory.class) {{
NPCFactory.getNPC();
result = new Person("expMock", 2);
}};

ClassRoom classRoom = new ClassRoom();
assertEquals("expMock", classRoom.getNPCName());
}

部分 mock/PartialMock

1
2
3
4
5
6
7
8
9
10
11
12
@Tested Person person;

@Test
public void person_name_jack() {
new Expectations(person) {{
person.getName();
result = "jack";
}};

assertEquals("jack", person.getName());
assertEquals(0, person.getAge());
}

partial 对非修饰类型有效吗?有效

new Expectations(ClassA.class) 会对这个 class 的所有实例生效,new Expectations(instance) 则只会对当前这个 instance 起作用,范围更精确

获取 Logger 引用做验证

如果你在 UT 中想要验证某条 log 有没有打印出来,你可以使用 @Capturing annotation。

相比于 @Mocked 而言,@Capturing 最大的特点是,他用于修饰 父类或者接口,那么他的所有实现类都会被 mocked 掉。对 log 的案例来说,我们为 Logger 这个 interface 加上这个注释之后,后续所有的实现都被 mock 掉,然后我们再做验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Tested Class
public class MySubscriber {

private static final Logger LOGGER = LogManager.getLogger(MySubscriber.class);

@Override
public void onEvent(Event event) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Start Process MySubscriber...");
LOGGER.info("End...");
}
}
}

// In UT
@Capturing
private Logger logger;

@Test
public void test_capturing_anno() {
new Expectations(ReadAuditSwitchHelper.class) {{
logger.isInfoEnabled();
result = true;
}};

subscriber.onEvent(context, event);

new Verifications() {{
logger.isInfoEnabled(); times=1;
List<String> capturedInfos = new ArrayList<>();
logger.info(withCapture(capturedInfos));

capturedInfos.stream().forEach(System.out::println);
}};
}

获取方法参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 如果是单个参数
new Verifications() {{
double d;
String s;
mock.doSomething(d = withCapture(), null, s = withCapture());

assertTrue(d > 0.0);
assertTrue(s.length() > 1);
}};

// 如果是多个参数
new Verifications() {{
List<DataObject> dataObjects = new ArrayList<>();
mock.doSomething(withCapture(dataObjects));

assertEquals(2, dataObjects.size());
DataObject data1 = dataObjects.get(0);
DataObject data2 = dataObjects.get(1);
// Perform arbitrary assertions on data1 and data2.
}};

@Mocked 导致 equals 方法失效

今天写 UT 的时候遇到一个问题,当我使用 @Mocked 修饰一个类时,这个类的所有引用都会被 mock 掉,虽然知道有这种特性,但是以前都没有碰到问题,忽视了,debug 花了好久。

示例如下:

准别两个简单的 MyBean 和 MyField, MyField 是 MyBean 的一个属性,并在声明时就做了初始化。

对应的 UT 可以 work,但当我对 MyField 添加 @Mocked 注解时,对应的 equals 方法会被抹去,UT 就挂了。

解决方案有两种:1. 不用 @Mocked; 2. 只做方法层面的 mock

对于第二种方法,testng 升级到 6.1 之后需要配合 @DataProvider 使用,变得麻烦了,也不知道后面的版本会不会修复这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class MyBean {
private MyField field = new MyField();

// getter/setter

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyBean myBean = (MyBean) o;
return Objects.equals(field, myBean.field);
}

@Override
public int hashCode() {
return Objects.hash(field);
}
}

public class MyField {
private String name;

// getter/setter

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyField myField = (MyField) o;
return Objects.equals(name, myField.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}

public class TestMockedAnno01 {
// @Mocked MyField field;

@Test
public void test() {
MyBean bean1 = new MyBean();
MyBean bean2 = new MyBean();

Assert.assertEquals(bean1, bean2);

}
}