单元测试(Junit+Jmockit)介绍及使用方法

名称解释

单元测试(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 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。

Jmock,Mockit,EasyMock,Unitils Mock

介绍略,有兴趣可自行搜索

Junit使用

在idea中,需要安装junit插件,具体安装及运行junit的方法参考:

https://jingyan.baidu.com/article/f7ff0bfccd661d2e26bb131a.html

然后在项目中引入jar包。


 junit
 junit
 4.12
 test

demo类:

public class Calculate {

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }
}

其测试类:

public class CalculateTest {
    private static Calculate calculate = null;

    @BeforeClass
    public static void beforeClass() {
        System.out.println("------------------------BeforeClass------------------------");
        calculate = new Calculate();
    }
    @AfterClass
    public static void afterClass() {
        System.out.println("------------------------AfterClass------------------------");
        calculate = null;
    }

    @Before
    public void setUp() throws Exception {
        System.out.println("-------Before Method-------");
    }
    @After
    public void tearDown() throws Exception {
        System.out.println("-------After Method-------");
    }

    @Test
    public void add() throws Exception {
        assertEquals(10, calculate.add(7, 3));
    }

    @Test
    public void subtract() throws Exception {
        assertEquals(4, calculate.subtract(7, 3));
    }
}

在测试类中右击鼠标选择Run即可执行Junit单元测试了。结果:

单元测试(Junit+Jmockit)介绍及使用方法_第1张图片

如果要运行多个测试类,那么在测试包上右键,点击右键菜单中的”run Tests in <包名>”即可。

注意事项:

1、测试方法上面必须使用@Test注解进行修饰。

2、测试方法必须使用public void 进行修饰,不能带有任何参数。

3、新建一个源代码目录用来存放测试代码。

4、测试类的包应该与被测试类的包保持一致。

5、测试单元中的每一个方法必须独立测试,每个测试方法之间不能有依赖。

6、测试类使用Test做为类名的后缀(非必要)。

7、测试用例是不是用来证明你是对的,而是用来证明你没有错。

常用注解

1、@BeforeClass所修饰的方法在所有方法加载前执行,而且他是静态的在类加载后就会执行该方法,在内存中只有一份实例,适合用来加载配置文件。

2、@AfterClass所修饰的方法在所有方法执行完毕之后执行,通常用来进行资源清理,例如关闭数据库连接。

3、@Before和@After在每个测试方法执行前都会执行一次。

4、@Test(excepted=XX.class) 在运行时忽略某个异常。

5、@Test(timeout=毫秒) 允许程序运行的时间。

6、@Ignore 所修饰的方法被测试器忽略。

Jmockit使用

pom.xml配置


 org.jmockit
 jmockit
 1.38
 test

Demo类

public class HelloJMockit {
    
    public String sayHello() {
        Locale locale = Locale.getDefault();
        if (locale.equals(Locale.CHINA)) {
            // 在中国,就说中文
            return "你好世界";
        } else {
            // 在其它国家,就说英文
            return "Hello World";
        }
    }
}

JMockit测试类:

public class HelloJMockitTest {
    @Test
    public void sayHelloCH() {
        new Expectations(Locale.class) {
            {
                Locale.getDefault();
                result = Locale.CHINA;
            }
        };
        // 断言说中文
        Assert.assertTrue("你好世界".equals((new HelloJMockit()).sayHello()));
    }

    @Test
    public void sayHelloUS() {
        new Expectations(Locale.class) {
            {
                Locale.getDefault();
                result = Locale.US;
            }
        };
        // 断言说英文
        Assert.assertTrue("Hello World".equals((new HelloJMockit()).sayHello()));
    }
}

在上面的例子中,对当前的位置Mock。即把测试代码的依赖抽象成期待(Expectations),在进行断言。

Jmockit如何模拟非静态对象:

    @Mocked
    HelloJMockit helloJMockit;

    @Test
    public void sayHello1() {
        // 录制(Record)
        new Expectations() {
            {
                helloJMockit.sayHello();
                // 期待上述调用的返回是"hello,david",而不是返回实际返回值
                result = "hello david";
            }
        };
        // 重放(Replay)
        String msg = helloJMockit.sayHello();
        Assert.assertTrue(msg.equals("hello david"));
        // 验证(Verification)
        new Verifications() {
            {
                helloJMockit.sayHello();
                // 验证helloJMockit.sayHello()这个方法调用了1次
                times = 1;
            }
        };
    }

上面的方法也可以按需写成如下格式:

public void sayHello1(@Mocked HelloJMockit helloJMockit) {…}

结构分析

通过上述例子可以看出,JMockit的程序结构包含了测试属性,测试方法。

测试方法体中又包含录制代码块,重放测试逻辑,验证代码块(Record-Replay-Verification): Record: 即先录制某类/对象的某个方法调用,在当输入什么时,返回什么。 Replay: 即重放测试逻辑。 Verification: 重放后的验证。比如验证某个方法有没有被调用,调用多少次。

常用注解及常用类

1. @Mocked

在上述例子中,我们用@Mocked修饰了测试属性HelloJMockit helloJMockit,表示helloJMockit这个测试属性,它的实例化,属性赋值,方法调用的返回值全部由JMockit来接管,接管后,helloJMockit的行为与HelloJMockit类定义的不一样了,而是由录制脚本来定义了。

@Mocked不仅能修饰一个类,也能修饰接口。@Mocked修饰的类/接口,是告诉JMockit,帮我生成一个Mocked对象,这个对象方法(包含静态方法)返回默认值。

2. @Tested & @Injectable

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

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

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

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

我们以电商网站下订单的场景为例,在买家下订单时,电商网站后台程序需要校验买家的身份,若下订单没有问题还要发短信给买家。

订单类:

public class OrderService {
    // 短信服务类,用于向某用户发短信。
    @Autowired MessageService messageService;
    // 用户服务类,用于校验某个用户是不是合法用户
    @Autowired UserService userService;
    // 下单
    public boolean submitOrder(long userId) {
        // 先校验用户身份
        if (!userService.check(userId)) {
            // 用户身份不合法
            return false;
        }
        // 下单
        this.saveOrder(order);// TODO 逻辑略…
        // 下单完成,给买家发短信
        if (this.messageService.sendMessage(userId, "下单成功")) {
            // 短信发送成功
            return true;
        }
        return false;
    }
}

测试类:

public class TestedAndInjectable {
    //@Tested修饰的类,表示是我们要测试对象。JMockit会帮我们实例化这个测试对象
    @Tested
    OrderService orderService;

    // 测试注入方式
    @Test
    public void testSubmitOrder(@Injectable MessageService messageService,
                                @Injectable UserCheckService userCheckService,
                                @Injectable Order testOrder) {
        long testUserId = 123l;
//实例化MessageService,userCheckService,通过OrderService属性,注入对象中;
        new Expectations() {
            {
                // 当向testUserId发短信时,假设都发成功了
                messageService.sendMessage(testUserId, anyString);
                result = true;
                // 当检验testUserId的身份时,假设该用户都是合法的
                userCheckService.check(testUserId);
                result = true; 
            }
        };
        Order testOrder = new Order("嘟嘟机器人", 996)
        Assert.assertTrue(orderService.submitOrder(testUserId, testOrder));
    }
}

3. @Capturing

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

4.MockUp & @Mock

这种方式非常简单,直接,很多程序员们都喜欢用,掌握了MockUp & @Mock能帮我们解决大部分的Mock场景。

案例如下:

class MockUpTest {
    @Test
    public void testMockUp() {
        // 对Java自带类Calendar的get方法进行定制
        // 只需要把Calendar类传入MockUp类的构造函数即可
        new MockUp(Calendar.class) {
            // 想Mock哪个方法,就给哪个方法加上@Mock, 没有@Mock的方法,不受影响
            @Mock
            public int get(int unit) {
                if (unit == Calendar.YEAR) {
                    return 2017;
                }
                if (unit == Calendar.MONDAY) {
                    return 1;
                }
                return 0;
            }
        };
        // 从此Calendar的get方法,就沿用你定制过的逻辑,而不是它原先的逻辑。
        Calendar cal = Calendar.getInstance(Locale.FRANCE);
        Assert.assertTrue(cal.get(Calendar.YEAR) == 2017);
        Assert.assertTrue(cal.get(Calendar.MONDAY) == 1);
        // Calendar的其它方法,不受影响
        Assert.assertTrue((cal.getFirstDayOfWeek() == Calendar.MONDAY));
    }
}

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

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

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

5. Expectations

Expectations的作用主要是用于录制。即录制类/对象的调用,返回值是什么。主要有两种使用方式:

a.通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制;

b.通过构建函数注入类/对象来录制.

6. Verifications

Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。

通常在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications() {{}}验证代码块。除非,你的测试程序关心类的某个方法有没有调用,调用多少次,你可以使用new Verifications() {{}}验证代码块。

常见用法

案例类(这个类有public,static,final,private方法):

class AnOrdinaryClass {
    // 普通方法
    public int ordinaryMethod() {
        return 1;
    }

    // 静态方法
    public static int staticMethod() {
        return 2;
    }

    // final方法
    public final int finalMethod() {
        return 3;
    }

    // private方法
    private int privateMethod() {
        return 4;
    }

    // 调用private方法
    public int callPrivateMethod() {
        return this.privateMethod();
    }
}

a. 测试类1(用Expectations来Mock):

class ClassMockingByExpectationsTest {
    @Test
    public void testClassMockingByExpectation() {
        AnOrdinaryClass instanceToRecord = new AnOrdinaryClass();
        new Expectations(AnOrdinaryClass.class) {
            {
                // mock普通方法
                instanceToRecord.ordinaryMethod();
                result = 11;
                // mock静态方法
                AnOrdinaryClass.staticMethod();
                result = 22;
                // mock final方法
                instanceToRecord.finalMethod();
                result = 33;
                // private方法无法用Expectations来Mock
            }
        };
        AnOrdinaryClass instance = new AnOrdinaryClass();
        Assert.assertTrue(instance.ordinaryMethod() == 11);
        Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
        Assert.assertTrue(instance.finalMethod() == 22);
        // 用Expectations无法mock private方法
        Assert.assertTrue(instance.callPrivateMethod() == 4);
    }
}

b. 测试类2(用MockUp来Mock):

class ClassMockingByMockUpTest {
    // AnOrdinaryClass的MockUp类,继承MockUp即可
    public static class AnOrdinaryClassMockUp extends MockUp {
        // Mock普通方法
        @Mock
        public int ordinaryMethod() {
            return 11;
        }
        
        // Mock静态方法
        @Mock
        public static int staticMethod() {
            return 22;
        }

        @Mock
        // Mock final方法
        public final int finalMethod() {
            return 33;
        }

        // Mock private方法
        @Mock
        private int privateMethod() {
            return 44;
        }
    }

    @Test
    public void testClassMockingByMockUp() {
        new AnOrdinaryClassMockUp();
        AnOrdinaryClass instance = new AnOrdinaryClass();
        // 普通方法被mock了
        Assert.assertTrue(instance.ordinaryMethod() == 11);
        // 静态方法被mock了
        Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
        // final方法被mock了
        Assert.assertTrue(instance.finalMethod() == 33);
        // private方法被mock了
        Assert.assertTrue(instance.callPrivateMethod() == 44);
    }
}

总结

建议使用MockUp & @Mock方法来写单元测试,JUnit的Assert类对测试结果的验证!并且灵活运用JMockit其它的Mock API。

##代码覆盖率
由于JMockit使用JavaSE5中的java.lang.instrument包开发,因此一般的单元测试覆盖率统计插件和工具对其无法工作,必须要借助自带的JMockit coverage才行。需要在项目pom.xml中如下定义


   maven-surefire-plugin
   
      -javaagent:"${settings.localRepository}\org\jmockit\jmockit\1.38\jmockit-1.38.jar=coverage"
      false
      
         html
         D:\temp\codecoverage-output
         all
      
   

配置完成后在运行测试代码,就会在控制台看到如下提示

JMockit: Coverage report written to D:\temp\codecoverage-output

进入到所示目录下(D:\temp\codecoverage-output),打开index.html,即可看到测试报告,如下图所示:
单元测试(Junit+Jmockit)介绍及使用方法_第2张图片

附录

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

你可能感兴趣的:(单元测试(Junit+Jmockit)介绍及使用方法)