1、简介


单元测试(unit testing)

是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,Java里单元指一个类。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

模拟测试(mock testing)

就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。

JUnit

是一个Java语言的单元测试框架。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

JMockit

JMockit 是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。所以他能解决当测试的代码包含了一些静态方法,未实现方法,未实现接口的问题。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。

Jmockit中文网:http://jmockit.cn/

Jmockit官网:http://jmockit.github.io/

2、常用注解


@Mocked
@Mocked不仅能修饰一个类,也能修饰接口。@Mocked修饰的普通类/接口/抽象类,是告诉JMockit,帮我生成一个Mocked对象,这个对象的方法(包含静态方法)都会返回return type的默认值,比如return type是int,则返回0,return type 是String,则返回null。

使用@Mocked时需要注意,如果是定义为测试类的成员变量,则被mock类的所有实例都是mock生成的,如下:

  1. public class HelloJMockitTest {
  2. @Tested
  3. HelloJMockit helloJMockit;
  4. //mock成员变量,Something类的所有实例均由mock生成
  5. @Mocked
  6. Something something;
  7. @Test
  8. public void sayHelloTest(){
  9. String msg = helloJMockit.sayHello();
  10. }
  11. }


如果是mocked方法参数,则只在该测试内的实例是mock生成的,如下:

  1. public class HelloJMockitTest {
  2. @Tested
  3. HelloJMockit helloJMockit;
  4. @Test
  5. public void sayHelloTest(@Mocked Something something){//作为方法参数,仅在该方法内是由mock生成实例
  6. something.doSomething();
  7. String msg = helloJMockit.sayHello();
  8. }
  9. }

@Tested & @Injectable

@Injectable 也是告诉 JMockit生成一个Mocked对象,但@Injectable只是针对其修饰的实例,而@Mocked是针对其修饰类的所有实例。此外,@Injectable对类的静态方法,构造函数没有影响。因为它只影响某一个实例。

@Tested修饰的类,表示是我们要测试对象,如果该对象没有赋值,JMockit会去实例化它。该注解不能实例化接口。

@Tested & @Injectable通常搭配使用。若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。

除了构造函数的注入,JMockit还会通过属性查找的方式,把@Injectable对象注入到@Tested对象中。注入的匹配规则:先类型,再名称(构造函数参数名,类的属性名)。若找到多个可以注入的@Injectable,则选择最优先定义的@Injectable对象。当然,我们的测试程序要尽量避免这种情况出现。因为给哪个测试属性/测试参数加@Injectable,是人为控制的。

@Capturing

@Capturing主要用于子类/实现类的Mock, 我们只知道父类或接口时,但我们需要控制它所有子类的行为时,子类可能有多个实现(可能有人工写的,也可能是AOP代理自动生成时)。就用@Capturing。

MockUp & @Mock

MockUp & @Mock比较适合于一个项目中,用于对一些通用类的Mock,以减少大量重复的new Exceptations{{}}代码。

在实际Mock场景中,我们需要灵活运用JMockit其它的Mock API。让我们的Mock程序简单,高效。

一个类有多个实例,但只对其中某1个实例进行mock的场景是MockUp & @Mock做不到的,这种时候就需要上述的@Capturing注解了。

Expectations

Expectations的作用主要是用于录制。即录制类/对象的调用,返回值是什么。主要有两种使用方式:
a.通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制;
b.通过构建函数注入类/对象来录制.

Verifications
Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。
通常在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications() {{}}验证代码块。除非,你的测试程序关心类的某个方法有没有调用,调用多少次,你可以使用new Verifications() {{}}验证代码块。

3、最佳实践


3.1、maven依赖

  1. <!-- 引入dependency -->
  2. <dependency>
  3. <groupId>org.jmockit</groupId>
  4. <artifactId>jmockit</artifactId>
  5. <version>1.49</version>
  6. <scope>test</scope>
  7. </dependency>
  8. <dependency>
  9. <groupId>junit</groupId>
  10. <artifactId>junit</artifactId>
  11. <version>4.13</version>
  12. <scope>test</scope>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-test</artifactId>
  17. <scope>test</scope>
  18. <exclusions>
  19. <exclusion>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-logging</artifactId>
  22. </exclusion>
  23. <exclusion>
  24. <groupId>org.junit.vintage</groupId>
  25. <artifactId>junit-vintage-engine</artifactId>
  26. </exclusion>
  27. </exclusions>
  28. </dependency>
  29. <!-- 同时引入plugin -->
  30. <plugin>
  31. <artifactId>maven-surefire-plugin</artifactId>
  32. <version>2.22.2</version>
  33. <configuration>
  34. <argLine>
  35. -javaagent:${settings.localRepository}/org/jmockit/jmockit/1.49/jmockit-1.49.jar=coverage
  36. </argLine>
  37. <disableXmlReport>true</disableXmlReport>
  38. </configuration>
  39. </plugin>

3.2、Mock静态方法

  1. //需要mock的静态方法
  2. public class StaticTarget {
  3. public static int m1() {
  4. return 1;
  5. }
  6. public static String m2(String s) {
  7. return 2;
  8. }
  9. }
  10. public class RunState {
  11. @Before
  12. public void stepUp(){
  13. //打桩需要被mock的静态方法
  14. new MockUp<StaticTarget>() {
  15. //被mock的静态方法,可以自己定义返回结果
  16. @Mock
  17. public String m2(String s) {
  18. return "TEST";
  19. }
  20. @Mock
  21. public int m1() {
  22. return 100;
  23. }
  24. };
  25. }
  26. //单元测试中调用
  27. @Test //import org.junit.jupiter.api.Test;
  28. public void test() {
  29. //调用静态方法
  30. int i = StaticTarget.m1();
  31. //判断返回值结果
  32. Assert.assertEquals(100, i));
  33. //调用静态方法
  34. String s = StaticTarget.m2();
  35. //判断返回值结果
  36. Assert.assertEquals("ATY", s);
  37. }
  38. }

3.3、Mock普通方法和私有方法

  1. //需要mock的普通方法
  2. public class CommonTarget {
  3. public int m3() {
  4. return 1;
  5. }
  6. public void m4(String s) {
  7. int i = 1 + 1;
  8. }
  9. }
  1. public class RunState {
  2. //打桩需要被mock的静态方法
  3. @Before
  4. public void stepUp(){
  5. //打桩需要mock的方法所属的类
  6. new MockUp<CommonTarget>() {
  7. @Mock
  8. public String m3(String s) {
  9. return "TEST";
  10. }
  11. @Mock
  12. public int m4() {
  13. return 100;
  14. }
  15. };
  16. }
  17. //单元测试中调用
  18. @Test
  19. public void test() {
  20. CommonTarget commonTarget = new CommonTarget();
  21. Assert.assertEquals("ATY", commonTarget.m3());
  22. Assert.assertEquals(100, commonTarget.m4());
  23. }
  24. }

tips:private方法不能被mock。在Jmockit的早期版本里,提供了反射的工具类来mock私有方法,但是在1.36版本之后,就移除了该功能,并且作者没有提供替代的方案。

3.4、被Mock的类没有无参构造函数


若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。

  1. /**
  2. ● 待测试的类
  3. */
  4. public class HelloJMockit {
  5. private static Locale locale;
  6. private int num=0;
  7. //不含有无参构造函数
  8. HelloJMockit(int num){
  9. this.num = num;
  10. }
  11. public String sayHello() {
  12. locale = Locale.getDefault();
  13. if (locale.equals(Locale.CHINA)) {
  14. // 在中国,就说中文
  15. return "你好,JMockit!";
  16. } else {
  17. // 在其它国家,就说英文
  18. return "Hello,JMockit!";
  19. }
  20. }
  21. }
  1. public class HelloJMockitTest {
  2. @Tested
  3. HelloJMockit helloJMockit;
  4. @Test
  5. public void sayHelloTest(@Injectable("123")int num){
  6. String msg = helloJMockit.sayHello();
  7. }
  8. }

3.5、对private方法进行单测


使用JMockit时,private方法不能被mock,1.36之前的版本可以mock私有方法,但是在之后的版本里,作者移除了该功能,并且没有给出相应的替代方案。
当需要对某个类中的private方法进行单元测试时,无法直接通过普通方式进行调用,这里推荐使用反射进行调用测试。

  1. public class PrivateMethodDemo {
  2. private String saySomething(String something){
  3. System.out.println(something);
  4. return something+" is said!";
  5. }
  6. }
  1. import mockit.Injectable;
  2. import mockit.Tested;
  3. import org.junit.Assert;
  4. import org.junit.jupiter.api.Test;
  5. import java.lang.reflect.InvocationTargetException;
  6. import java.lang.reflect.Method;
  7. public class PrivateMethodDemoTest {
  8. @Tested
  9. PrivateMethodDemo privateMethodDemo;
  10. @Test
  11. public void saySomethingTest(@Injectable("hello")String something) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  12. Method method = PrivateMethodDemo.class.getDeclaredMethod("saySomething",String.class);
  13. method.setAccessible(true);
  14. Assert.assertEquals(something+" is said!",method.invoke(privateMethodDemo,something));
  15. }
  16. }

3.6、仅在指定条件下mock,其他条件执行原逻辑


在某些场景下,我们需要仅在某种条件下执行mock逻辑,其他条件执行原逻辑,这里以平台常用的SPI工具类FactoriesLoader举例。
在待测试类中,通过spi机制来获取一个数据库连接管理工厂的实例,如果不对其进行mock,会抛出空指针异常;
待模拟方法如下:

  1. public class FactoriesLoader{
  2. //...some code...
  3. public static <T> T getDefaultFactory(Class<T> factoryClass) {
  4. SPI spi = factoryClass.getAnnotation(SPI.class);
  5. if (spi == null)
  6. return null;
  7. String id = spi.defaultId();
  8. if (id != null) {
  9. Map<String, T> factoryInstances = getFactories(factoryClass);
  10. return factoryInstances.get(id);
  11. }
  12. return null;
  13. }
  14. //...some code...
  15. }


先创建一个工厂类,然后通过mock生成该类的实例对象。

  1. //interface也可以被@Mocked
  2. public interface DBConnectionManagerFactory {
  3. public DBConnectionManager getDBConnectionManager();
  4. }


然后模拟SPI工具类的方法,由于该方法在执行过程中,还有其他地方也要调用,比如获取日志输出对象,所以在模拟的时候,要求只有当此处调用该方法时,才执行mock逻辑,否则应执行原逻辑。测试代码如下:

  1. public class EdspTransactionManagerTest {
  2. //待测试类
  3. @Tested
  4. EdspTransactionManager edspTransactionManager;
  5. //模拟的工厂类
  6. @Mocked
  7. DBConnectionManagerFactory dbConnectionManagerFactory;
  8. @Test
  9. public void getDBConnectionEnableTest() throws NoSuchMethodException,
  10. InvocationTargetException, IllegalAccessException {
  11. //模拟SPI工具类
  12. new MockUp<FactoriesLoader>() {
  13. @Mock
  14. public <T> T getDefaultFactory(Invocation invocation, Class<T> factoryClass) {//factoryClass是原方法的参数,invocation是JMcokit的参数
  15. //只有满足条件,才执行模拟逻辑
  16. if (factoryClass == DBConnectionManagerFactory.class) {
  17. return (T) dbConnectionManagerFactory;
  18. }
  19. //否则执行原逻辑
  20. return invocation.proceed(factoryClass);
  21. }
  22. };
  23. }
  24. }

3.7、直接在测试方法的参数列表里注入mock对象


在测试方法的参数列表里加入参数,这在以往的印象中是不行的,但是Jmockit允许我们这样做,使用@Mocked和@Injectable注解即可实现。如下:

  1. public class InjectableDemoTest {
  2. @Tested
  3. InjectableDemo injectableDemoTested;
  4. @Test
  5. public void doSomethingWhitParamTest(@Injectable User user, @Mocked JobEntity jobEntity){
  6. new Expectations(){
  7. {
  8. user.getUsername();
  9. result = "Davi";
  10. }
  11. {
  12. jobEntity.getId();
  13. result = 1;
  14. }
  15. };
  16. String msg = injectableDemoTested.doSomethingWhitParam(user);
  17. Assert.assertEquals("hello,Davi",msg);
  18. Assert.assertEquals(1,jobEntity.getId());
  19. }
  20. }

此处有个需要注意的点,使用@Injectable时,如果不存在@Tested,那么Injectable的对象不会被mock,会为null。

3.8、@Mocked和@Injectable的区别


使用@Mocked注解修饰一个变量实例,则该实例所属的类的所有实例都会被mock,包括类的静态方法、构造函数都会被mock;
使用@Injectable注解修饰一个变量实例,只有该变量实例会被mock,该变量实例所属类的其他实例不受影响,类的静态方法、构造函数也不受影响。
@Mocked注解可以mock接口、抽象类、普通类;而@Injectable注解只能mock普通类
如下:

  1. @Test
  2. public void showDifferenceTest(@Injectable User user1, @Mocked JobEntity jobEntity1){
  3. new Expectations(){
  4. {
  5. user1.getUsername();
  6. result = "张三";
  7. }
  8. {
  9. jobEntity1.getId();
  10. result = 1;
  11. }
  12. };
  13. User user2 = new User();//重新new的一个对象
  14. user2.setUsername("李四");
  15. System.out.println("user1:"+user1.getUsername());
  16. System.out.println("user2:"+user2.getUsername());
  17. JobEntity jobEntity2 = new JobEntity();//重新new的一个对象
  18. jobEntity2.setId(2);
  19. System.out.println("jobEntity1:"+jobEntity1.getId());
  20. System.out.println("jobEntity2:"+jobEntity2.getId());
  21. }
  22. //控制台输出:
  23. //user1:张三
  24. //user2:李四
  25. //jobEntity1:1
  26. //jobEntity2:1

3.9、模拟返回值类型为void的方法


返回值类型为void的方法,不能使用Expectation来模拟,可以通过MockUp来模拟如下:

  1. public class VoidMethodDemo {
  2. //要被模拟的void类型方法
  3. public void doSomething(){
  4. System.out.println("do something with void return type");
  5. }
  6. }
  7. public class VoidMethodDemoTest {
  8. @Tested
  9. VoidMethodDemo voidMethodDemo;
  10. @Test
  11. public void doSomethingTest(){
  12. new MockUp<VoidMethodDemo>(){
  13. @Mock
  14. public void doSomething(){
  15. System.out.println("had been mocked!");
  16. }
  17. };
  18. voidMethodDemo.doSomething();
  19. }
  20. }

3.10、模拟抛出Exception


当需要测试一些异常场景时,需要模拟某个方法运行时抛出Exception。如下:

  1. public class ExceptioneDemoTest {
  2. @Tested
  3. User user;
  4. /**
  5. * 使用result返回一个exception实例
  6. */
  7. @Test
  8. public void doSomethingTest2(){
  9. new Expectations(user){
  10. {
  11. user.getUsername();
  12. result = new RuntimeException();
  13. }
  14. };
  15. Assert.assertThrows(RuntimeException.class,()->{user.getUsername();});
  16. }
  17. /**
  18. * 使用MockUp&Mock直接mock原方法抛出异常
  19. */
  20. @Test
  21. public void exceptionTest(){
  22. new MockUp<User>(){
  23. @Mock
  24. public int getId() {
  25. throw new RuntimeException();
  26. }
  27. };
  28. Assert.assertThrows(RuntimeException.class,()->{user.getId();});
  29. }
  30. }

3.11、当在spring环境中Autowired一个List变量时,该如何mock


在spring中,容器中的bean都是单例bean,而且并不会有List类型在容器中,但是当容器中有多个同类型的bean时,spring自动注入@Autowired就支持用list来接收。JMockit在这方面也对spring做了支持。代码如下:

  1. public class ListFieldDemo {
  2. //如果没有@AutoWired注解,则mock不生效
  3. @Autowired
  4. private List<User> userList;
  5. public List<User> getUserList(){
  6. return userList;
  7. }
  8. }
  1. public class ListFieldDemoTest {
  2. @Tested
  3. ListFieldDemo listFieldDemo;
  4. @Injectable
  5. User user1;
  6. @Injectable
  7. User user2;
  8. @Test
  9. public void getUserListTest(){
  10. List<User> userList = listFieldDemo.getUserList();
  11. Assert.assertNotNull(userList);
  12. System.out.println(userList.size());//size值是2,jmockit会像spring一样自动把user1和user2注入到userList中
  13. }
  14. }

3.12、mock代码块和静态代码块

  1. public class StaticCodeBlockDemo {
  2. {
  3. System.out.println("代码块执行了");
  4. }
  5. static {
  6. System.out.println("静态代码块执行了");
  7. }
  8. public void sayHello(){
  9. System.out.println("hello");
  10. }
  11. }
  1. public class StaticCodeBlockDemoTest {
  2. @Tested
  3. StaticCodeBlockDemo staticCodeBlockDemo;
  4. @BeforeEach
  5. public void mockStaticCodeBlock(){
  6. new MockUp<StaticCodeBlockDemo>(){
  7. //模拟代码块
  8. @Mock
  9. public void $init(){
  10. System.out.println("代码块被mock了");
  11. }
  12. //模拟静态代码块
  13. @Mock
  14. public void $clinit(){
  15. System.out.println("静态代码块被mock了");
  16. }
  17. };
  18. }
  19. @Test
  20. public void getUserTest(){
  21. staticCodeBlockDemo.sayHello();
  22. }
  23. }

3.13、使用Verifications进行验证


JMockit的Record-Replay-Verify中,使用Expectations进行模拟,使用Verifications进行验证。通过Verifications,可以验证模拟方法被调用的次数,通过VerificationInOrder可以验证模拟方法的调用顺序。代码如下:

  1. public class VerificationsDemo {
  2. public String method1(){
  3. return "method1 ";
  4. }
  5. public String method2(){
  6. return "method2";
  7. }
  8. public String method3(){
  9. return "method3";
  10. }
  11. }
  1. public class VerificationsDemoTest {
  2. @Tested
  3. VerificationsDemo verificationsDemo;
  4. /**
  5. * 验证调用次数
  6. */
  7. @Test
  8. public void verficationDemo(){
  9. //Record
  10. new Expectations(verificationsDemo){
  11. {
  12. verificationsDemo.method1();
  13. result = "mocked";
  14. }
  15. };
  16. //Replay
  17. System.out.println(verificationsDemo.method1());
  18. //System.out.println(verificationsDemo.method1());
  19. //System.out.println(verificationsDemo.method1());
  20. //Verify
  21. new Verifications(){
  22. {
  23. verificationsDemo.method1();
  24. times = 3;
  25. }
  26. //{
  27. // verificationsDemo.method1();
  28. // maxTimes = 2;
  29. //}
  30. //{
  31. // verificationsDemo.method1();
  32. // minTimes = 2;
  33. //}
  34. };
  35. }
  36. /**
  37. * 验证调用顺序
  38. */
  39. @Test
  40. public void verificationInOrderTest(){
  41. new Expectations(verificationsDemo){
  42. {
  43. verificationsDemo.method1();
  44. result = "method1 mocked";
  45. }
  46. {
  47. verificationsDemo.method2();
  48. result = "method2 mocked";
  49. }
  50. {
  51. verificationsDemo.method3();
  52. result = "method3 mocked";
  53. }
  54. };
  55. System.out.println(verificationsDemo.method1());
  56. System.out.println(verificationsDemo.method2());
  57. System.out.println(verificationsDemo.method3());
  58. new VerificationsInOrder(){
  59. //验证调用顺序
  60. {
  61. verificationsDemo.method1();
  62. verificationsDemo.method2();
  63. verificationsDemo.method3();
  64. }
  65. };
  66. }
  67. }

3.14 使用Delegate委托来mock

Delegate类可以起到一个委托的效果,将原方法的逻辑委托给一个自定义方法,从而达到mock的效果。
定义一个DelegateService类:

  1. public class DelegateService {
  2. //被委托mock的方法
  3. public int intReturningMethod(String arg1, int arg2){
  4. return 1;
  5. }
  6. }

定义一个测试类,在其中调用上面的委托方法:

  1. public class DelegateDemo {
  2. public int testService(){
  3. DelegateService delegateService = new DelegateService();
  4. return delegateService.intReturningMethod("hello",2);
  5. }
  6. }

测试代码如下:

  1. public class DelegateDemoTest {
  2. @Tested
  3. DelegateDemo delegateDemo;
  4. //使用@Mocked修饰的类的所有实例都会走mock逻辑
  5. @Mocked
  6. DelegateService delegateService;
  7. @Test
  8. public void testServiceTest(){
  9. new Expectations() {{
  10. delegateService.intReturningMethod(anyString,anyInt );
  11. result = new Delegate() {
  12. int aDelegateMethod(String s,int i ) {
  13. System.out.println( "代理方法执行了,原入参:arg1:"+s+" arg2:"+i );
  14. return 100;
  15. }
  16. };
  17. }};
  18. System.out.println( delegateDemo.testService() );
  19. }
  20. }

测试结果:
image.png

3.15、使用any参数

anyString、anyInt、anyLong等。。。。
“any”可以匹配Object类型的参数,也可以强转它,比如:”(User)any”可以得到一个User对象示例

3.16、使用with方法

@Test
void someTestMethod(@Mocked DependencyAbc abc) {
   DataItem item = new DataItem(...);

   new Expectations() {{
      // 只要第二个参数不为null就可以匹配上
      abc.voidMethod("str", (List<?>) withNotNull());
      result = 1000;

      // 第一个参数要和item是同一个对象实例
      // 第二个参数要包含"xyz"字符串
      // 同时满足以上两个条件才能匹配上
      abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
      result = 100;
   }};

   cut.doSomething(item);

   new Verifications() {{
      // withAny(T) :只要参数类型为T就会匹配上
      abc.anotherVoidMethod(withAny(1L));
   }};
}