JAVA自動化之Junit單元測試框架詳解

一、JUnit概述&配置

1、Junit是什麼?

Junit是一個Java 程式語言的開源測試框架,用於編寫和運行測試。官網 地址://junit.org/junit4/

2、Maven配置

?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>junit</groupId>
    <artifactId>junitTest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <!-- //mvnrepository.com/artifact/junit/junit -->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

二、Assertions 斷言

JUnit提供了一些輔助的函數,用來判斷被測試的方法是否如我們預期的效果一樣正常執行。這些輔助函數被稱之為斷言。
常用斷言:

方法 示例 功能
assertArrayEquals assertArrayEquals(“message”, expected, actual); 判斷兩個數組是否相等
assertEquals assertEquals(“message”, “text”, “text”); 判斷兩個對象是否相等
assertFalse assertFalse(“failure – should be false”, false); 判斷表達式是否為false
testAssertTrue assertTrue(“failure – should be true”, true); 判斷表達式是否為true
assertNotNull assertNotNull(“should not be null”, new Object()); 判斷是否非空
assertNull assertNull(“should be null”, null); 判斷是否為空
assertNotSame assertNotSame(“should not be same Object”, new Object(), new Object()); 判斷是否為不同對象
assertSame assertSame(“should be same”, aNumber, aNumber); 判斷是否為同一對象
…… …… ……

 

三、Test Runners 測試運行器

JUnit中所有的測試方法都是由測試運行器負責執行。當一個類被@RunWith注釋或拓展了一個@RunWith注釋的類,JUnit將會使用引用的類來執行測試,而不是使用JUnit內置的運行器。

  org.junit.runner.JUnitCore.runClasses(TestClass1.class, ...);

Specialized Runners:
Suite:Suite是一個標準的運行器,允許手動構建包含來自許多類的測試集。
Parameterized:Parameterized是一個實現參數化測試的標準運行器。運行參數化測試類時,測試方法和測試數據進行合併來創建測試實例。
Categories:Categories運行器來制定分類,定義測試被包含或排除。

 

四、Aggregating tests in suites 套件

測試套件用於捆綁幾個單元測試用例並且一起執行他們,使用@RunWith 和 @Suite 註解。

@RunWith(Suite.class)
@Suite.SuiteClasses({AssertTests.class, CalculatorTest.class})
public class SuiteTest {
    // the class remains empty, used only as a holder for the above annotations
}

五、Test execution order 執行順序

要改變測試執行的順序只需要在測試類上使用 @FixMethodOrder註解,並指定一個可用的MethodSorter即可:
@FixMethodOrder(MethodSorters.DEFAULT):JUnit默認使用一個確定的,但不可預測的順序
@FixMethodOrder(MethodSorters.JVM): 保留測試方法的執行順序為JVM返回的順序,每次測試的執行順序有可能會所不同
@FixMethodOrder(MethodSorters.NAME_ASCENDING):根據測試方法的方法名排序,按照詞典排序規則(ASC從小到大遞增)

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ExecutionOrderTest {
    @Test
    public void testB() {
        System.out.println("second");
    }
    @Test
    public void testA() {
        System.out.println("first");
    }
    @Test
    public void testC() {
        System.out.println("third");
    }
}

運行結果:

first
second
third

六、Expected Exceptions 異常測試

用於測試某一方法是否拋出了正確的異常。
1、@Test(expected=xxx)方式:當拋出的異常與expected參數指定的異常相同時,測試通過。
2、try…fail…catch…方式:捕捉具體待測語句的異常資訊並斷言,當沒有異常被拋出的時候fail方法會被調用,輸出測試失敗的資訊。
3、ExpectedException Rule方式:使用Rule標記來指定一個ExpectedException,並在測試相應操作之前指定期望的Exception類型。

public class ExpectedExceptionsTest {
    //方法一:@Test(expected=xxx)
    @Test(expected = IndexOutOfBoundsException.class)
    public void empty() {
        new ArrayList<Object>().get(0);
    }

    //方法二:try...fail...catch...  當沒有異常被拋出的時候fail方法會被調用,輸出測試失敗的資訊。
    @Test
    public void testExceptionMessage() {
        try {
            new ArrayList<Object>().get(0);
            fail("Expected an IndexOutOfBoundsException to be thrown");
        } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
            assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
        }
    }

    //方法三:在測試之前使用Rule標記來指定一個ExpectedException,並在測試相應操作之前指定期望的Exception類型(如IndexOutOfBoundException.class)
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
        List<Object> list = new ArrayList<Object>();
        thrown.expect(IndexOutOfBoundsException.class);
        thrown.expectMessage("Index: 0, Size: 0");
        list.get(0);
    }
}

七、Matchers and assertThat

JUnit4.4引入了Hamcrest框架,Hamcest提供了一套匹配符Matcher,這些匹配符更接近自然語言,可讀性高,更加靈活。並且使用全新的斷言語法assertThat,結合Hamcrest提供的匹配符,只用這一個方法,就可以實現所有的測試。
assertThat語法:
assertThat(T actual, Matcher matcher);
assertThat(String reason, T actual, Matcher matcher);
其中reason為斷言失敗時的輸出資訊,actual為斷言的值或對象,matcher為斷言的匹配器,裡面的邏輯決定了給定的actual對象滿不滿足斷言。
Matchers詳見:
//hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matchers.html

八、Ignoring tests 忽略測試

  • 方法用 @Ignore 註解了將不會被執行
  • 類用 @Ignore 註解後,其下所有測試方法將不會被執行
public class IgnoreTest {
    @Ignore("Test is ignored as a demonstration")
    @Test
    public void testSame() {
        assertThat(1, is(1));
    }
}

九、Timeout for tests 超時測試

@Timeout 註解用來測試特定方法的執行時間。如果測試方法的執行時間大於指定的超時參數,測試方法將拋出異常,測試結果為失敗。指定的超時參數單位為毫秒
1、@Test註解上的timeout參數,作用域為方法,單位毫秒

@Test(timeout = 2000)
    public void testSleepForTooLong() throws Exception {
        log += "ran1";
        TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
    }

2、Timeout Rule,作用域為測試類

public class TimeoutTests {
    public static String log;
    private final CountDownLatch latch = new CountDownLatch(1);

    @Rule
    public Timeout globalTimeout = Timeout.seconds(3); // 3 seconds max per method tested

    @Test
    public void testSleepForTooLong() throws Exception {
        log += "ran1";
        TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
    }

    @Test
    public void testBlockForever() throws Exception {
        log += "ran2";
        latch.await(); // will block
    }
}

十、Parameterized tests 參數化測試

參數化測試允許開發人員使用不同的值反覆運行同一個測試。創建參數化測試步驟:

  • 用 @RunWith(Parameterized.class) 來注釋 test 類。
  • 創建一個由 @Parameters 注釋的公共的靜態方法,它返回一個對象的集合(數組)來作為測試數據集合。
  • 創建一個公共的構造函數,接受測試數據。
  • 為每一列測試數據創建一個實例變數。
  • 用實例變數作為測試數據的來源來創建測試用例。
    1、Constructor方式
@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters(name = "{index}: fib({0})={1}")
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
        });
    }
    private int fInput;
    private int fExpected;
    public FibonacciTest(int input, int expected) {
        this.fInput = input;
        this.fExpected = expected;
    }
    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}

2、Field injection方式

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters(name = "{index}: fib({0})={1}")
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
        });
    }
    @Parameter // first data value (0) is default
    public /* NOT private */ int fInput;
    @Parameter(1)
    public /* NOT private */ int fExpected;
    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}

十一、Assumptions with assume 假定測試

使用Assumptions類中的假設方法時,當假設不成立時會報錯,但是測試會顯示被ignore忽略執行。也就是當一個類中有多個測試方法時,其中一個假設測試方法假設失敗,其他的測試方法全部成功,那麼該測試類也會顯示測試成功。
假設方法適用於:在不影響測試是否成功的結果的情況下根據不同情況執行相關程式碼。

public class AssumptionsTest {
    @Test
    public void testAssumTrue() {
        System.out.println("test");
        assumeTrue(3>5);
        //該方法中下面所有的程式碼在上面假設的條件成立後執行
        //如果上述假設不成立,則會忽略執行該行下面的程式碼,並報錯
        System.out.println("assume is true!");
    }
    @Test
    public void testAssumFalse(){
        assumeFalse(3>5);
        System.out.println("assume is true!");
    }
  }

以下語法JUnit5支援:

@Test
    public void testAssumTrueMessage() {
        assumeTrue(3<5,
                //第二個參數為當第一個參數不成立時,輸出的自定義錯誤資訊
                () -> "Aborting test: not on developer workstation");
        System.out.println("assume is true!");
    }
    @Test
    public void testAssumeTrueLambda(){
        //這個方法的第一個參數為函數式介面,無參數返回值為boolean
        assumeTrue(()->{
            System.out.println("in assumeTrue");
            boolean flag = false;
            return flag;
        });
        System.out.println("out assumeTrue");
    }
    @Test
    public void testAssumThat() {
        assumingThat(3>5,
                () -> {
                    //與上述方法不同的是,僅當前面假設成立時,才會執行這裡面的語句
                    //且只會影響到該lambda表達式中的程式碼
                    assertEquals(2, 2);
                });
        //此處的斷言不受上述assumingThat限制,在所有情況下都會執行
        System.out.println("no effect");
        assertEquals("a string", "a string");
    }

十二、Rules 規則

一個JUnit Rule就是一個實現了TestRule的類,用來在每個測試方法的執行前後執行一些程式碼。
1、框架自帶的Rule
JUnit自帶很多已經實現過好了的JUnit Rule,比如Timeout,ExpectedException等等。
2、自定義Rule
自定義一個Rule就是implement一個TestRule interface,實現一個叫apply()的方法。
例:在測試方法運行之前,記錄測試方法所在的類名和方法名,然後在測試方法運行之後列印出來。

public class MethodNameExample implements TestRule {
    @Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                //base.evaluate()之前為測試方法運行之前所做操作
                String className = description.getClassName();
                String methodName = description.getMethodName();
                //運行測試方法
                base.evaluate();
                //base.evaluate()之後為測試方法運行之後所做操作
                System.out.println("Class name: "+className +", method name: "+methodName);
            }
        };
    }
}

 

public class RuleTest2 {
    @Rule
    public MethodNameExample methodNameExample = new MethodNameExample();
    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
    @Test
    public void mulitiplication_isCorrect() throws Exception {
        assertEquals(4, 2 * 2);
    }
}

十三、Theories

在參數化測試中,我們需要給定所有具體的測試數據組。而在Theories測試中,用戶只需給定了一些數據,JUnit自動利用這些數據組合出各種各種可能的組合來執行測試。

1、內置實現

(1)@DataPoints註解靜態變數方式

@RunWith(Theories.class)
public class TheoryTest {
    //允許的最大誤差
    private static final double DELTA = 0.01;
    /*@DataPoints註解靜態變數*/
    @DataPoint
    public static int ZERO = 0;
    @DataPoint
    public static int TWO = 2;
    @DataPoint
    public static int EIGHT = 8;
    //標誌這個測試為Theory測試
    @Theory
    public void testDivide(int dividend, int divisor) {
        //跳過除數為0的case
        assumeThat(divisor, not(0));
        //Calculator.divide(dividend, divisor)方法返回他們相除的結果
        assertEquals(dividend / divisor, Calculator.divide(dividend, divisor), DELTA);
        System.out.println("Passed with: dividend=" + dividend + ", divisor=" + divisor);
    }
}

(2)@DataPoints註解靜態方法方式

@RunWith(Theories.class)
public class TheoryTest {
    //允許的最大誤差
    private static final double DELTA = 0.01;
    /*@DataPoints註解一個靜態方法*/
    @DataPoints
    public static int[] getTestData() {
        return new int[]{0, 2, 8};
    }
    //標誌這個測試為Theory測試
    @Theory
    public void testDivide(int dividend, int divisor) {
        //跳過除數為0的case
        assumeThat(divisor, not(0));
        //Calculator.divide(dividend, divisor)方法返回他們相除的結果
        assertEquals(dividend / divisor, Calculator.divide(dividend, divisor), DELTA);
        System.out.println("Passed with: dividend=" + dividend + ", divisor=" + divisor);
    }
}

@DataPoint用於註解靜態變數(或靜態方法),表示這個變數是個數據點。當執行testDivide這個Theory測試時,JUnit會把所有的DataPoint數據兩兩組合,形成一組組的測試數據,並用這些數據分別執行測試。執行上面的測試會輸出以下結果:

Passed with: dividend=0, divisor=2
Passed with: dividend=0, divisor=8
Passed with: dividend=2, divisor=2
Passed with: dividend=2, divisor=8
Passed with: dividend=8, divisor=2
Passed with: dividend=8, divisor=8

(3)如果需要限定某個參數,可以使用@TestOn註解

import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.suppliers.TestedOn;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(Theories.class)
public class TheoryTest {
    //允許的最大誤差
    private static final double DELTA = 0.01;
    //如果需要限定某個參數,可以使用@TestOn註解
    @Theory
    public void testDivide2(
            @TestedOn(ints = {0, 2, 8}) int dividend,
            @TestedOn(ints = {2, 8}) int divisor
    ) {
        assertEquals(dividend / divisor, Calculator.divide(dividend, divisor), DELTA);
        System.out.println("Passed with: dividend=" + dividend + ", divisor=" + divisor);
    }
}

2、自定義實現

JUnit默認只提供了一個int型的簡單 Parameter Supplier 實現,而Theory機制真正的價值在於,能參考@TestedOn的做法,相對簡單的完全自定義出可重用 Parameter Supplier,適應於各種複雜要求的限定範圍參數值測試場景,滿足開發者所需的高度動態自定義範圍取值自動化測試,同時保留與一般@Test相同的強大兼容性。
例:
(1)定義annotation註解介面Between

@Retention(RetentionPolicy.RUNTIME)
// 聲明註解介面所使用的委託處理類
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between{
    // 聲明所有可用參數,效果為 @Between([first = int,] last = int)
    int first() default 0;  // 聲明默認值
    int last();
}

(2)定義委託處理類 BetweenSupplier

public class BetweenSupplier extends ParameterSupplier {
    @Override
    public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
        // 自定義實參值列表
        List<PotentialAssignment> list = new ArrayList<PotentialAssignment>();
        // 獲取註解變數
        Between between = sig.getAnnotation(Between.class);
        // 獲取通過註解@Between傳入的first值
        int first = between.first();
        // 獲取通過註解@Between傳入的last值
        int last = between.last();
        for (int i = first; i <= last; i++) {
            // PotentialAssignment.forValue(String name, Object value)
            // name為value的描述標記,沒實際作用
            // value為實參可選值
            list.add(PotentialAssignment.forValue("name", i));
        }
        return list;
    }
}

(3)調用方式

@RunWith(Theories.class)
public class TheoryDefinedTest {
    @Theory
    public final void test(@Between(last = 0) int i, @Between(first = 3, last= 10) int j) {
        // i 取值為 0(first默認=0,last=0),j 取值為 3-10
        System.out.println("i="+i+"  j="+j);
    }
}

(4)運行結果

i=0  j=3
i=0  j=4
i=0  j=5
i=0  j=6
i=0  j=7
i=0  j=8
i=0  j=9
i=0  j=10

十四、Test fixtures

Test Fixture是指一個測試運行所需的固定環境,也是就是測試運行之前所需的穩定的、公共的可重複的運行環境,這個「環境」不僅可以是數據,也可以指對被測軟體的準備,例如實例化被測方法所依賴的類、載入資料庫等等。

  • @Before – 在每個@Test方法之前運行
  • @After – 在每個@Test方法之後運行
  • @BeforeClass – 在所有的@Test方法之前運行一次
  • @AfterClass – 在所有的@Test方之後運行一次
    註:
    1、如果創建一個子類繼承有fixture註解的父類,那麼子類中的@Before方法會在測試方法之前、父類的@Before執行之後執行。
    2、如果@Before方法里拋出了異常,@Test方法會跳過,但是@After還是會執行
    3、每個測試方法都會在單獨的測試類的實例裡面運行,@BeforeClass在測試實例創建之前執行
public class FixtureTest {
    private static int quantity = 0;
    public FixtureTest() {
        quantity++;
    }
    @BeforeClass
    public static void breforeTestOnlyOnce() throws Exception {
        System.out.println("Run before all test only once..."+ quantity);
    }
    @AfterClass
    public static void afterTestOnlyOnce() throws Exception {
        System.out.println("Run after all test only once..."+ quantity);
    }
    @Before
    public void beforePerTest() {
        System.out.println("Run before per test ..."+ quantity);
    }
    @After
    public void afterPerTest() {
        System.out.println("Run after per test ..."+ quantity);
    }
    //Test Method
    @Test
    public void testOne() {
        System.out.println("testOne Start..."+ quantity);
    }
    @Test
    public void testTwo() {
        System.out.println("testTwo Start..."+ quantity);
    }
}

運行結果:

Run before all test only once...0
Run before per test ...1
testOne Start...1
Run after per test ...1
Run before per test ...2
testTwo Start...2
Run after per test ...2
Run after all test only once...2

十五、Categories 用例分類

category 和 testSuite的比較:testSuite是類級分組(xx.class),category是用例級分組(@Test),category是testSuite的升級
category使用步驟:
1、創建好測試類,及測試類中的測試用例
2、創建介面:按用例的分組來創建
3、@Category註解:將用例用@Category註解分組
4、創建類來執行這些分組的類

public interface FastTests { /* category marker */ }
public interface SlowTests { /* category marker */ }

public class A {
  @Test
  public void a() {
    fail();
  }
  @Category(SlowTests.class)
  @Test
  public void b() {
  }
}

@Category({SlowTests.class, FastTests.class})
public class B {
  @Test
  public void c() {
  }
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@SuiteClasses( { A.class, B.class }) // Note that Categories is a kind of Suite
public class SlowTestSuite {
  // Will run A.b and B.c, but not A.a
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@ExcludeCategory(FastTests.class)
@SuiteClasses( { A.class, B.class }) // Note that Categories is a kind of Suite
public class SlowTestSuite {
  // Will run A.b, but not A.a or B.c
}

十六、總結

如果你覺得此文對你有幫助,如果你對軟體測試、介面測試、自動化測試、面試經驗交流感興趣歡迎加入軟體測試技術群:695458161,群里發放的免費資料都是筆者十多年測試生涯的精華。還有同行大神一起交流技術哦。