1. 程式人生 > >JAVA自動化之Junit單元測試框架詳解

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

一、JUnit概述&配置

1、Junit是什麼?

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

2、Maven配置

?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
    <groupId>junit</groupId>
    <artifactId>junitTest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <!-- https://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詳見:
http://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,群裡發放的免費資料都是筆者十多年測試生涯的精華。還有同行大神一起交流技術