单元测试(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包。

<dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>4.12</version>
 <scope>test</scope>
</dependency>

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单元测试了。结果:

image.png

如果要运行多个测试类,那么在测试包上右键,点击右键菜单中的”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配置

<dependency>
 <groupId>org.jmockit</groupId>
 <artifactId>jmockit</artifactId>
 <version>1.38</version>
 <scope>test</scope>
</dependency>

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>(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<AnOrdinaryClass> {
        // 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中如下定义

<plugin>
   <artifactId>maven-surefire-plugin</artifactId>
   <configuration>
      <argLine>-javaagent:"${settings.localRepository}\org\jmockit\jmockit\1.38\jmockit-1.38.jar=coverage"</argLine>
      <disableXmlReport>false</disableXmlReport>
      <systemPropertyVariables>
         <coverage-output>html</coverage-output>
         <coverage-outputDir>D:\temp\codecoverage-output</coverage-outputDir>
         <coverage-metrics>all</coverage-metrics>
      </systemPropertyVariables>
   </configuration>
</plugin>

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

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

进入到所示目录下(D:\temp\codecoverage-output),打开index.html,即可看到测试报告,如下图所示:


image.png

附录

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 基础概念 whatJMockit是一款Java类/接口/对象的Mock工具,目前广泛应用于Java应用程序的单元测...
    visionarywind阅读 5,392评论 0 1
  • 白盒测试又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试是一种测试用例设计方法,盒子指的是...
    yongliu1229阅读 2,571评论 0 4
  • 单元测试实践背景 测试环境定位bug时,需要测试同学协助手动发起相关业务URL请求,开发进行远程调试问题:1、远程...
    Zeng_小洲阅读 7,468评论 0 4
  • 我有一个好朋友李倩,从小就失去双亲,姥姥一手将她带大。大学毕业后在家乡哈尔滨脱同学找到一份不怎么样的工作。...
    喵女人阅读 728评论 0 1
  • 1业务还是模凌两可,不能准确定位 2做事不够细心,咋咋呼呼的 3主观能动性差,要领导推着走 好不容易通过某种途径走...
    笨的连头都不如阅读 294评论 0 0