记录一下工作中常用到的 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); } @Test public void test02 () { System.out.println(new Teacher("Jack" ).name); }
如果做 method level 的 mock, 只作用 case 本身
1 2 3 4 5 @Test public void test (@Mocked Teacher teacher) { System.out.println(teacher.name); } @Test public void test02 () { System.out.println(new Teacher("Jack" ).name); }
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) {} }
修复方法:将 @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(); } }
Mockup 工厂方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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 @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 @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 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..." ); } } } @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 ); }};
@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(); @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; @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 { @Test public void test () { MyBean bean1 = new MyBean(); MyBean bean2 = new MyBean(); Assert.assertEquals(bean1, bean2); } }