• JUnit单元测试


    前段时间面试被问及很多关于单元测试的相关知识,然后大佬着重给我强调了关于单元测试的重要性,最近一直各方面学习关于单元测试的知识以作补充强化。


    什么是单元测试

    单元测试是为了测试某一个代码单元而写的测试代码。这里的“代码单元”我通常将它认为一个具体类中的某个方法;所以,也可以这样理解:

    单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码;

    常见的Java单元测试框架有:JUnit、TestNG等;


    单元测试不是集成测试

    单元测试只是测试一个方法单元,它不是测试一整个流程;

    举例:一个Login页面,上面有两个输入框和一个button。两个输入框分别用于输入用户名和密码。点击button以后,有一个UserManager会去执行performlogin操作,然后将结果返回,更新页面。

    那么我们给这个东西做单元测试的时候,不是测这一整个login流程。这种整个流程的测试:给两个输入框设置正确的用户名和密码,点击login button, 最后页面得到更新。叫做集成测试,而不是单元测试。


    JUnit4

    一个测试方法主要包括三个部分:

    1. setup
    2. 执行操作
    3. 验证结果

    一个测试类的很多测试方法可能需要相同的setup,所以为我们提供了便捷方法。对于JUnit4,是通过 @Before 来实现的:

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
        @Test
        public void testAdd() throws Exception {
            int sum = mCalculator.add(1, 2);
            assertEquals(3, sum);  //为了简洁,往往会static import Assert里面的所有方法。
        }
    
        @Test
        public void testMultiply() throws Exception {
            int product = mCalculator.multiply(2, 4);
            assertEquals(8, product);
        }
    
    }

    如果一个方法被 @Before 修饰过了,那么在每个测试方法调用之前,这个方法都会得到调用。

    对应于 @Before 的,有一个 @After ,作用估计你也猜得到,那就是每个测试方法运行结束之后,会得到运行的方法。比如一个测试文件操作的类,那么在它的测试类中,可能 @Before 里面需要去打开一个文件,而每个测试方法运行结束之后,都需要去close这个文件。这个时候就可以把文件close的操作放在 @After 里面,让它自动去执行。

    @BeforeClass 和 @AfterClass 。 @BeforeClass 的作用是,在跑一个测试类的所有测试方法之前,会执行一次被 @BeforeClass 修饰的方法,执行完所有测试方法之后,会执行一遍被 @AfterClass 修饰的方法。这两个方法可以用来setup和release一些公共的资源,需要注意的是,被这两个annotation修饰的方法必须是静态的。

    对于第三部“验证结果”,则一般是通过一些assert方法来完成的。JUnit为我们提供的assert方法,多数都在 Assert 这个类里面。最常用的那些如下:

    assertEquals(expected, actual)
    验证expected的值跟actual是一样的,如果是一样的话,测试通过,不然的话,测试失败。如果传入的是object,那么这里的对比用的是equals();

    assertEquals(expected, actual, tolerance)
    这里传入的expected和actual是float或double类型的,大家知道计算机表示浮点型数据都有一定的偏差,所以哪怕理论上他们是相等的,但是用计算机表示出来则可能不是,所以这里运行传入一个偏差值。如果两个数的差异在这个偏差值之内,则测试通过,否者测试失败。

    assertTrue(boolean condition)
    验证contidion的值是true;

    assertFalse(boolean condition)
    验证contidion的值是false;

    assertNull(Object obj)
    验证obj的值是null;

    assertNotNull(Object obj)
    验证obj的值不是null;

    assertSame(expected, actual)
    验证expected和actual是同一个对象,即指向同一个对象

    assertNotSame(expected, actual)
    验证expected和actual不是同一个对象,即指向不同的对象;

    fail()
    让测试方法失败;

    注意:上面的每一个方法,都有一个重载的方法,可以在前面加一个String类型的参数,表示如果验证失败的话,将用这个字符串作为失败的结果报告。

    比如:

    assertEquals(“Current user Id should be 1”, 1, currentUser.id());

    当 currentUser.id() 的值不是1的时候,在结果报道里面将显示”Current user Id should be 1”,这样可以让测试结果更具有可读性,更清楚错误的原因是什么。

    比较有意思的是最后一个方法, fail() ,你或许会好奇,这个有什么用呢?其实这个在很多情况下还是有用的,比如最明显的一个作用就是,你可以验证你的测试代码真的是跑了的。

    此外,它还有另外一个重要作用,那就是验证某个被测试的方法会正确的抛出异常。


    JUnit的其他功能

    Ignore一些测试方法;

    很多时候,因为某些原因(比如正式代码还没有实现等),我们可能想让JUnit忽略某些方法,让它在跑所有测试方法的时候不要跑这个测试方法。要达到这个目的也很简单,只需要在要被忽略的测试方法前面加上 @Ignore 就可以了,如下:

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
        // Omit testAdd() and testMultiply() for brevity
    
        @Test
        @Ignore("not implemented yet")
        public void testFactorial() {
        }
    }

    验证方法会抛出某些异常

    有的时候,抛出异常是一个方法正确工作的一部分。比如一个除法函数,当除数是0的时候,它应该抛出异常,告诉外界,传入的被除数是0,示例代码如下:

    public class Calculator {
    
        // Omit testAdd() and testMultiply() for brevity
    
        public double divide(double divident, double dividor) {
            if (dividor == 0) throw new IllegalArgumentException("Dividor cannot be 0");
    
            return divident / dividor;
        }}

    那么如何测试当传入的除数是0的时候,这个方法应该抛出 IllegalArgumentException 异常呢?

    在Junit中,可以通过给 @Test annotation传入一个expected参数来达到这个目的,如下:

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
        // Omit testAdd() and testMultiply() for brevity
    
        @Test(expected = IllegalArgumentException.class)
        public void test() {
            mCalculator.divide(4, 0);
        }
    
    }

    @Test(expected = IllegalArgumentException.class) 表示验证这个测试方法将抛出 IllegalArgumentException 异常,如果没有抛出的话,则测试失败。


    测量代码覆盖率

    代码覆盖率衡量(以百分比表示)了在运行单元测试时执行的代码量。通常,高覆盖率的代码包含未检测到的错误的几率要低,因为其更多的源代码在测试过程中被执行。测量代码覆盖率的一些最佳做法包括:

    • 使用代码覆盖工具,如Clover,Corbetura,JaCoCo或Sonar。使用工具可以提高测试质量,因为这些工具可以指出未经测试的代码区域,让你能够开发开发额外的测试来覆盖这些领域。
    • 每当写入新功能时,立即写新的测试覆盖。
    • 确保有测试用例覆盖代码的所有分支,即if / else语句。

    下面的 concat 方法接受布尔值作为输入,并且仅当布尔值为true时附加传递两个字符串:

    < class="hljs typescript">public String concat(boolean append, String a,String b) {
            String result = null;
            If (append) {
                result = a + b;
                                }
            return result.toLowerCase();
    }

    以下是上述方法的测试用例:

    <class="hljs less">@Test
    public void testStringUtil() {
         String result = stringUtil.concat(true, "Hello ", "World");
         System.out.println("Result is "+result);
    }

    在这种情况下,执行测试的值为true。当测试执行时,它将通过。当代码覆盖率工具运行时,它将显示100%的代码覆盖率,因为 concat 方法中的所有代码都被执行。但是,如果测试执行的值为false,则将抛出 NullPointerException 。所以100%的代码覆盖率并不真正表明测试覆盖了所有场景,也不能说明测试良好。


    尽可能将测试数据外部化

    在JUnit4之前,测试用例要运行的数据必须硬编码到测试用例中。这导致了限制,为了使用不同的数据运行测试,测试用例代码必须修改。但是,JUnit4以及TestNG支持外部化测试数据,以便可以针对不同的数据集运行测试用例,而无需更改源代码。

    下面的 MathChecker 类有方法可以检查一个数字是否是奇数:

    < class="hljs kotlin">public class MathChecker {
            public Boolean isOdd(int n) {
                if (n%2 != 0) {
                    return true;
                } else {
                    return false;
                }
            }
        }

    以下是MathChecker类的TestNG测试用例:

    < class="hljs less">public class MathCheckerTest {
            private MathChecker checker;
            @BeforeMethod
            public void beforeMethod() {
              checker = new MathChecker();
            }
            @Test
            @Parameters("num")
            public void isOdd(int num) { 
              System.out.println("Running test for "+num);
              Boolean result = checker.isOdd(num);
              Assert.assertEquals(result, new Boolean(true));
            }
        }

    TestNG

    以下是testng.xml(用于TestNG的配置文件),它具有要为其执行测试的数据:

    < class="hljs xml"><?xml version="1.0" encoding="UTF-8"?>
        <suite name="ParameterExampleSuite" parallel="false">
        <test name="MathCheckerTest">
        <classes>
          <parameter name="num" value="3"></parameter>
          <class name="com.stormpath.demo.MathCheckerTest"/>
        </classes>
         </test>
         <test name="MathCheckerTest1">
        <classes>
          <parameter name="num" value="7"></parameter>
          <class name="com.stormpath.demo.MathCheckerTest"/>
        </classes>
         </test>
        </suite>

    可以看出,在这种情况下,测试将执行两次,值3和7各一次。除了通过XML配置文件指定测试数据之外,还可以通过DataProvider注释在类中提供测试数据。

    JUnit

    与TestNG类似,测试数据也可以外部化用于JUnit。以下是与上述相同MathChecker类的JUnit测试用例:

    < class="hljs java">@RunWith(Parameterized.class)
        public class MathCheckerTest {
         private int inputNumber;
         private Boolean expected;
         private MathChecker mathChecker;
         @Before
         public void setup(){
             mathChecker = new MathChecker();
         }
            // Inject via constructor
            public MathCheckerTest(int inputNumber, Boolean expected) {
                this.inputNumber = inputNumber;
                this.expected = expected;
            }
            @Parameterized.Parameters
            public static Collection<Object[]> getTestData() {
                return Arrays.asList(new Object[][]{
                        {1, true},
                        {2, false},
                        {3, true},
                        {4, false},
                        {5, true}
                });
            }
            @Test
            public void testisOdd() {
                System.out.println("Running test for:"+inputNumber);
                assertEquals(mathChecker.isOdd(inputNumber), expected);
            }
        }

    可以看出,要对其执行测试的测试数据由getTestData()方法指定。此方法可以轻松地修改为从外部文件读取数据,而不是硬编码数据。


    使用断言而不是Print语句

    许多新手开发人员习惯于在每行代码之后编写System.out.println语句来验证代码是否正确执行。这种做法常常扩展到单元测试,从而导致测试代码变得杂乱。除了混乱,这需要开发人员手动干预去验证控制台上打印的输出,以检查测试是否成功运行。更好的方法是使用自动指示测试结果的断言。

    下面的 StringUti 类是一个简单类,有一个连接两个输入字符串并返回结果的方法:

    < class="hljs typescript">public class StringUtil {
            public String concat(String a,String b) {
                return a + b;
            }
        }

    以下是上述方法的两个单元测试:

    < class="hljs less">@Test
        public void testStringUtil_Bad() {
             String result = stringUtil.concat("Hello ", "World");
             System.out.println("Result is "+result);
        }
        @Test
        public void testStringUtil_Good() {
             String result = stringUtil.concat("Hello ", "World");
             assertEquals("Hello World", result);
        }

    testStringUtil_Bad将始终传递,因为它没有断言。开发人员需要手动地在控制台验证测试的输出。如果方法返回错误的结果并且不需要开发人员干预,则testStringUtil_Good将失败。


    构建具有确定性结果的测试

    一些方法不具有确定性结果,即该方法的输出不是预先知道的,并且每一次都可以改变。例如,考虑以下代码,它有一个复杂的函数和一个计算执行复杂函数所需时间(以毫秒为单位)的方法:

    < class="hljs java">public class DemoLogic {
        private void veryComplexFunction(){
            //This is a complex function that has a lot of database access and is time consuming
            //To demo this method, I am going to add a Thread.sleep for a random number of milliseconds
            try {
                int time = (int) (Math.random()*100);
                Thread.sleep(time);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        public long calculateTime(){
            long time = 0;
            long before = System.currentTimeMillis();
            veryComplexFunction();
            long after = System.currentTimeMillis();
            time = after - before;
            return time;
        }
        }0

    在这种情况下,每次执行 calculateTime 方法时,它将返回一个不同的值。为该方法编写测试用例不会有任何用处,因为该方法的输出是可变的。因此,测试方法将不能验证任何特定执行的输出。


    除了正面情景外,还要测试负面情景和边缘情况

    通常,开发人员会花费大量的时间和精力编写测试用例,以确保应用程序按预期工作。然而,测试负面测试用例也很重要。负面测试用例指的是测试系统是否可以处理无效数据的测试用例。例如,考虑一个简单的函数,它能读取长度为8的字母数字值,由用户键入。除了字母数字值,应测试以下负面测试用例:

    • 用户指定非字母数字值,如特殊字符。
    • 用户指定空值。
    • 用户指定大于或小于8个字符的值。

    类似地,边界测试用例测试系统是否适用于极端值。例如,如果用户希望输入从1到100的数字值,则1和100是边界值,对这些值进行测试系统是非常重要的。


    部分内容原文地址:

    http://blog.csdn.net/u014743697/article/details/54691594

  • 相关阅读:
    其他
    聚类算法:ISODATA算法
    大神博客
    Fiddldr 教程之:HTTP协议详解(转)
    设计模式之装饰模式的复习
    NOIP 2011 聪明的质监员
    CSP-S2020/NOIP2020模板总结(Updating)
    CSP-S2020/NOIP2020复习指南
    洛谷 U137412 高斯的小宇宙
    NOIP2020模板测试题大全
  • 原文地址:https://www.cnblogs.com/aixing/p/13327601.html
Copyright © 2020-2023  润新知