• 浅谈单元测试 Junit4


    在这里插入图片描述

    JUint是Java编程语言的单元测试框架,用于编写和运行可重复的自动化测试。本文主要针对Junit4要点进行梳理总结。

    一、浅谈单元测试

    1.1 什么是单元测试

      单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

    1.2 为什么要写单元测试

      这其实是问单元测试能带来什么好处,之所以把这个问题放在这里讨论,是因为需要清楚单元测试的使用场景,以及它做得到和做不到的。我们在写好一个函数或者类以后,需要检验我们的程序是否存在bug或者是否满足我们的需求,通常的做法就是将写好的函数在mian方法中调用,输入一些测试用例进行检验。当要检验的方法数量较少时,这种方法可行,但是当我们有大量的函数需要验证时,该方法就显得笨重且繁琐,往往需要我们人工检查输出的结果是否正确等,比较混乱。因此,单元测试就是用来解决这种繁琐问题的。使用单元测试可以有效地降低程序出错的机率,提供准确的文档,并帮助我们改进设计方案等等。以下列举了一些我为什么使用单元测试的好处:

    • 允许你对代码做出较任何改变,因为你了解单元测试会在你的预期之中。
    • 单元测试可以有效地降低程序出现BUG的机率。
    • 帮助你更深入地理解代码,因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等等。
    • 允许在任何时候代码重构,而不必担心破坏现有的代码,这使得我们编写程序更灵活。
    • 确保你的代码的健壮性,因为所有的测试都是通过了的。

    1.3 什么时候写单元测试

    写单元测试的时机不外乎三种情况:

    • 一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的;
    • 二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
    • 三是编写完功能代码再写单元测试。我的实践经验告诉我,事后编写的单元测试“粒度”都比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。

    个人比较推荐单元测试与具体实现代码同步进行这个方案的,只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。

    二、初识 Junit4

    2.1 什么是JUnit

    JUint是Java编程语言的单元测试框架,用于编写和运行可重复的自动化测试。其具有如下特点:

    • 提供注解来识别测试方法。
    • 提供断言来测试预期结果。
    • JUnit 测试允许你编写代码更快,并能提高质量。
    • JUnit 优雅简洁。没那么复杂,花费时间较少。
    • JUnit测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
    • JUnit测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
    • JUnit在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。

    2.2 官方资料

    最好的资料依然在Junit官方网站,以下我帮你总结下Junit相关的官方网址。

    网址说明
    https://junit.org/junit4/官网地址
    https://junit.org/junit4/docs/current/user-guide/官方入门文档
    https://github.com/junit-team官方github

    2.3 常用注解

    Junit4 注解提供了书写单元测试的基本功能,这里列出一些常用的注解,如下表所示:

    注解说明
    @Test测试注解,标记一个方法可以作为一个测试用例
    @Ignore暂不执行该方法
    @BeforeClass在所有测试之前,只执行一次,且必须为static void
    @AfterClass在所有测试之后,只执行一次,且必须为 static void
    @Before该方法必须在类中的每个测试之前执,以便执行某些必要的先决条件
    @After该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)
    @Runwith测试类的初始化。在测试类的开头标注,表明运行环境
    @Parameters指定测试类的测试数据集合
    @FixMethodOrder指定测试方法的执行顺序

    三、编写单元测试

    3.1 引入相关依赖

      现在主流的IDE比如IDEA或者Eclipse都提供了对JUnit4的支持,可以非常方便的使用JUnit4。当你在代码中添加了@Test注解,然后使用IDE的自动补全功能时,一般情况下IDE会弹出对话框询问你是否将JUnit4库添加到项目的类路径下。当然也可以自己手动添加JUnit4的依赖。如果使用Maven,添加如下一段:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
        <modelVersion>4.0.0modelVersion>
        <groupId>org.examplegroupId>
        <artifactId>MavenDemoartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <dependencies>
            <dependency>
                <groupId>junitgroupId>
                <artifactId>junitartifactId>
                <version>4.12version>
            dependency>
        dependencies>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.2 编写测试类

      一个 JUnit 测试是一个在专用于测试的类中的一个方法, 并且这个方法被 @org.junit.Test 注解标注。我们只需要在我们要测试的方法上加上 @Test 注解,那么这个方法就会被当做一个单元测试,单独去运行,我们来试一下。

    import org.junit.Test;
    import static org.junit.Assert.assertEquals;
    
    public class HelloWorldTest {
        @Test
        public void firstTest() {
            assertEquals(2, 1 + 1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    @Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。在 Junit 3中,是通过对测试类和测试方法的命名来确定是否是测试,且所有的测试类必须继承junit的测试基类。在 Junit 4中,定义一个测试方法变得简单很多,只需要在方法前加上@Test就行了。注意:测试方法必须是public void,即公共、无返回数据

    3.3 测试生命周期

      JUnit4测试用例的完整的生命周期要经历几个阶段,他们分别是类级初始化资源处理、方法级初始化资源处理、执行测试用例中的方法、方法级销毁资源处理、类级销毁资源处理等。这几个阶段分别有对应的注解所标注,如下表

    注解说明
    @BeforeClass该方法只执行一次,并且在所有方法之前执行,例如创建数据库连接、读取文件等。
    @AfterClass该方法只执行一次,并且在所有方法之后执行。通常用来对资源进行释放,比如数据库连接的关闭等。
    @Before该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
    @After该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操

    简单来说,使用@BeforeClass 和 @AfterClass 两个注解标注的方法会在所有测试方法执行前后各执行一次,使用@Before 和 @After 两个注解标注的方法会在每个测试方法执行前后都执行一次。

    import org.junit.*;
    
    public class StandardTest {
    
        @BeforeClass
        public static void beforeClass() {
            System.out.println("@BeforeClass 方法被执行");
        }
    
        @AfterClass
        public static void afterClass() {
            System.out.println("@AfterClass 方法被执行");
        }
    
        @Before
        public void before() {
            System.out.println("@Before 单元测试开始前相关操作...");
        }
    
        @After
        public void after() {
            System.out.println("@After 单元测试结束后相关操作...");
        }
    
        @Test
        public void testCase1() {
            System.out.println("in test case 1");
        }
    
        @Test
        public void testCase2() {
            System.out.println("测试生命周期");
        }
    }
    
    • 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

    这里有如下几点注意事项:

    • 父类的@BeforeClass注解方法会在子类的@BeforeClass注解方法执行前执行。
    • 父类@Before修饰的方法会在子类@Before修饰的方法执行前执行。
    • 父类@After修饰的方法会在子类@After修饰的方法执行后执行。
    • 父类中的被@AfterClass注解方法修饰的方法会在子类的@AfterClass注解修饰的方法执行之后才会被执行。

    3.4 断言测试

    断言测试注解如下表所示:

    断言描述
    void assertEquals()如果比较的两个对象是相等的,此方法将正常返回,否则测试将中止
    void assertTrue()断言if条件或变量是否是true
    void assertFalse()断言if条件或变量是否是false
    void assertNotNull()断言一个对象不为空(null)
    void assertNull()断言一个对象为空(null)
    void assertSame()断言两个对象引用相同的对象
    void assertNotSame()断言两个对象不是引用同一个对象
    void assertArrayEquals()比较两个数组,如果它们相等,则该方法将继续进行不会发出错误,否则中止测试
    import org.hamcrest.core.CombinableMatcher;
    import org.junit.Test;
    import java.util.Arrays;
    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;
    
    public class AssertionTest {
    
        @Test
        public void testAssertArrayEquals() {
            byte[] expected = "trial".getBytes();
            byte[] actual = "trial".getBytes();
            assertArrayEquals("failure - byte arrays not same", expected, actual);
        }
    
        @Test
        public void testAssertEquals() {
            assertEquals("failure - strings are not equal", "text", "text");
        }
    
        @Test
        public void testAssertFalse() {
            assertFalse("failure - should be false", false);
        }
    
        @Test
        public void testAssertNotNull() {
            assertNotNull("should not be null", new Object());
        }
    
        @Test
        public void testAssertNotSame() {
            assertNotSame("should not be same Object", new Object(), new Object());
        }
    
        @Test
        public void testAssertNull() {
            assertNull("should be null", null);
        }
    
        @Test
        public void testAssertSame() {
            Integer aNumber = Integer.valueOf(768);
            assertSame("should be same", aNumber, aNumber);
        }
    
        @Test
        public void testAssertThatBothContainsString() {
            assertThat("albumen", both(containsString("a")).and(containsString("b")));
        }
    
        @Test
        public void testAssertThatHasItems() {
            assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
        }
    
        @Test
        public void testAssertThatEveryItemContainsString() {
            assertThat(Arrays.asList(new String[]{"fun", "ban", "net"}), everyItem(containsString("n")));
        }
    
        @Test
        public void testAssertThatHamcrestCoreMatchers() {
            assertThat("good", allOf(equalTo("good"), startsWith("good")));
            assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
            assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
            assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
            assertThat(new Object(), not(sameInstance(new Object())));
        }
    
        @Test
        public void testAssertTrue() {
            assertTrue("failure - should be true", true);
        }
    }
    
    • 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
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    3.5 参数化测试

      为保证单元测试的严谨性,通常会模拟不同的测试数据来测试方法的处理能力,为此我们需要编写大量的单元测试的方法。可是这些测试方法都是大同小异的,它们的代码结构都是相同的,不同的仅仅是测试数据和期望值。为解决这个问题,Junit 4 引入了一个新的功能参数化测试,允许开发人员使用不同的值反复运行同一个测试。参数化测试主要解决一次性进行多个测试用例的测试。其主要思想是,将多个测试用例按照,{输入值,输出值}(输入值可以是多个)的列表方式进行测试。
      要进行参数化测试,需要在类上面指定如下的运行器:@RunWith (Parameterized.class),然后,在提供数据的方法上加上一个@Parameters注解,这个方法必须是静态static的,并且返回一个集合Collection。

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.junit.runners.Parameterized;
    import java.util.*;
    import static org.junit.Assert.assertEquals;
    
    @RunWith(Parameterized.class)
    public class MyJUnit4ParaTest {
        private int input1;
        private int input2;
        private int expected;
    
        /**
         * 准备数据。数据的准备需要在一个方法中进行,该方法需要满足一定的要求:
         * 1)该方法必须由Parameters注解修饰
         * 2)该方法必须为public static的
         * 3)该方法必须返回Collection类型
         * 4)该方法的名字不做要求
         * 5)该方法没有参数
         */
        @Parameterized.Parameters
        public static Collection<Object[]> prepareData() {
            Object[][] object = {{3, 1, 4}, {36, 6, 42}, {0, 4, 4}};
            return Arrays.asList(object);
        }
    
        /**
         * 构造方法
         *
         * 必须要为类的所有字段赋值,不管是不是都用到!否则,Junit会出错。
         */
        public MyJUnit4ParaTest(int input1, int input2, int expected) {
            this.input1 = input1;
            this.input2 = input2;
            this.expected = expected;
        }
    
        @Test
        public void testDiv() {
            Calculator calc = new Calculator();
            // input1,input2,expected分别对应二维数组元素的第一个,第二个和第三个元素
            int result = calc.add(input1, input2);   
            assertEquals(expected, result);
        }
    }
    
    class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    }
    
    • 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
    • 51

      一般来说,在一个类里面只执行一个测试方法。因为所准备的数据是无法共用的,这就要求,所要测试的方法是大数据量的方法,所以才有必要写一个参数化测试。而在实际开发中,参数化测试用到的并不是特别多。

    四、结语

      到这里,想必你对 JUnit 4 也有了基本的了解和掌握,都说单元测试是提升软件质量,提升研发效率的必备环节,从会用 JUnit 4 写单元测试开始,培养写测试代码的习惯,在不断实践中提升自身的开发效率,让写出来的代码有更质量的保证。

  • 相关阅读:
    十四、队列函数
    c语言 编程及答案
    使用CCS软件查看PID曲线
    视频集中存储/直播点播平台EasyDSS点播文件分类功能新升级
    文件输入输出处理(二)-字节流
    2023年中国少儿在线英语教育分类、市场规模及发展趋势分析[图]
    基于Android驾校驾考助手 java驾照考试系统
    蓝桥杯备赛-上学迟到
    动态中位数(对顶堆)
    防抖和节流
  • 原文地址:https://blog.csdn.net/duleilewuhen/article/details/126808122