Junit4單元測試


Junit4單元測試

官方文檔

第一部分 用法

1.1 常見功能

典型配置:

/*用於配置spring Boot中測試的環境*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyBlogApplication.class)
/* 開啟事務,測試完成默認自動回滾,不會弄臟數據庫 */
@Transactional

public class WhoHaveWhatTagsMapperTest {

    @BeforeClass
    public static void beforeClass() {
    }

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void insertWhoHaveWhatTags() throws Exception {
    }

    @Test
    public void selectBlogByTag() throws Exception {
    }

    @Test
    public void deleteWhoHaveWhatTags() throws Exception {
    }

}
  • @Test:把一個方法標記為測試方法
    兩個屬性:
    excepted;表示測試在執行中期望拋出的異常類型,如果不拋出,反而報錯。
    timeout:超時拋出異常。單位毫秒
    @Test(timeout = 2000)
    @Test(expected = Exception.class)
    public void testFactorialException() throws Exception {
        new Math().factorial(-1);
        fail("factorial參數為負數沒有拋出異常");
    }
  • @Before:每一個測試方法執行前自動調用一次

  • @After:每一個測試方法執行完自動調用一次

  • @BeforeClass:所有測試方法執行前執行一次,在測試類還沒有實例化就已經被加載,所以用static修飾

  • @AfterClass:所有測試方法執行完執行一次,在測試類還沒有實例化就已經被加載,所以用static修飾

  • @Ignore:暫不執行該測試方法

  • setup方法主要實現測試前的初始化工作

  • teardown方法主要實現測試完成后垃圾回收工作!

setup方法主要實現測試前的初始化工作,teardown方法主要實現測試完成后垃圾回收工作!
測試方法的聲明要求:名字可以隨便取,沒有任何限制,但是返回值必須為void,而且不能有任何參數。

  • 參數化測試
    你可能遇到過這樣的函數,它的參數有許多特殊值,或者說他的參數分為很多個區域。比如測試一下“計算一個數的平方”這個函數,暫且分三類:正數、0、負數。測試代碼如下:
public class CalculatorTest {
	private static Calculator calculator = new Calculator();//這個類要自己寫

	@Before
	public void clearCalculator() {
		calculator.clear();
	}
	@Test
	public void square1() {
		calculator.square(2);
		assertEquals(4, calculator.getResult());
	}
	@Test
	public void square2() {
		calculator.square(0);
		assertEquals(0, calculator.getResult());
	}
	@Test
	public void square3() {
		calculator.square(-3);
		assertEquals(9, calculator.getResult());
	}
}

為了簡化類似的測試,JUnit4提出了“參數化測試”的概念,只寫一個測試函數,把這若干種情況作為參數傳遞進去,一次性的完成測試。代碼如下:

@RunWith(Parameterized.class)
public class SquareTest {
	private static Calculator calculator = new Calculator();
	private int param;
	private int result;

	@Parameters
        //輸入的參數和預期的結果
	public static Collection data() {
		return Arrays.asList(new Object[][] { { 2, 4 }, { 0, 0 }, { -3, 9 }, });
	}
	// 構造函數,對變量進行初始化
	public SquareTest(int param, int result) {
		this.param = param;
		this.result = result;
	}
	@Test
	public void square() {
		calculator.square(param);
		assertEquals(result, calculator.getResult());
	}
}
  • 打包測試
    在一個項目中,只寫一個測試類是不可能的,我們會寫出很多很多個測試類。可是這些測試類必須一個一個的執行,也是比較麻煩的事情。鑒於此,JUnit為我們提供了打包測試的功能,將所有需要運行的測試類集中起來,一次性的運行完畢,大大的方便了我們的測試工作。具體代碼如下:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, SquareTest.class})
public class AllCalculatorTests{}

從上面可以看到,這個功能也需要使用一個特殊的Runner,因此我們需要向@RunWith標注傳遞一個參數Suite.class。同時,我們還需要另外一個標注@Suite(SuiteClasses),來表明這個類是一個打包測試類。我們把需要打包的類作為參數傳遞給該標注就可以了。有了這兩個標注之后,就已經完整的表達了所有的含義,因此下面的類已經無關緊要,隨便起一個類名,內容全部為空既可。

  • Assume 對待測方法的參數進行合法性校驗的,如果校驗不合格則直接拋異常,而不執行測試。
    Assume提供的校驗規則如下:
      assumeTrue/assumeFalse、 assumeNotNull、 assumeThat、 assumeNoException 

例如:(通過下述代碼也可以看到,要使用參數,則應使用@Theory注解)

@Theory
public void printAge(String name, int age){
        Assume.assumeTrue(age > 0);//如果參數age<=0,會拋AssumptionViolatedException異常
        System.out.println(String.format("%s's Name is %s.", name, age));
}
  • Assert 是Junit提供的斷言,與Assume不同,Assert是對測試結果的校驗,它提供的檢驗規則如下:
    AssertTrue、AssertFalse:結果的true、false。
    AssertThat:使用Matcher做自定義的校驗。
    AssertEquals、AssertNotEquals:判斷兩個對象是否相等。
    AssertNull、AssertNotNull:判斷對象是否為空。
    AssertSame:判斷兩個對象是否為同一個,不同於equals這里是使用“==”判斷。
    AssertArrayEquals:判斷兩個數組是否相等。

  • 多線程測試
    JUnit4的Test寫好以后,對於一些集成度比較高的測試用例,還希望完成並發訪問情況下的測試,但是,JUnit4缺省情況沒有提供,可以通過自己寫一個main函數,然后創建幾個線程,在幾個線程中同時運行測試用例進行測試,來模擬並發訪問的情況,具體例子:

public class TestExample {

    @Test
    public void testMethod() {
    System.out.println("test success!");
    }
}

public class PerfomanceTest {
    public static void main(String[] args) {
        new Thread() {
            public void run() { 
                // JUnitCore.runClasses(new Class[] { TestExample.class });           (1)
                // new JUnitCore().run(Request.method(TestExample.class, "testMethod"));        (2)
            }
        }.start();
    }
}

注:標志1或標志2中只要用一種就可以測試。

1.2 Spring的@Transactional注解用法

參考:http://www.cnblogs.com/yepei/p/4716112.html
事務管理對於企業應用來說是至關重要的,即使出現異常情況,它也可以保證數據的一致性。
Spring Framework對事務管理提供了一致的抽象,其特點如下:

  • 為不同的事務API提供一致的編程模型,比如JTA(Java Transaction API), JDBC, Hibernate, JPA(Java Persistence API和JDO(Java Data Objects)
  • 支持聲明式事務管理,特別是基於注解的聲明式事務管理,簡單易用
  • 提供比其他事務API如JTA更簡單的編程式事務管理API
  • 與spring數據訪問抽象的完美集成

事務管理方式
spring支持編程式事務管理和聲明式事務管理兩種方式。
編程式事務管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務管理,spring推薦使用TransactionTemplate。

聲明式事務管理建立在AOP之上的。其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。聲明式事務最大的優點就是不需要通過編程的方式管理事務,這樣就不需要在業務邏輯代碼中摻雜事務管理的代碼,只需在配置文件中做相關的事務規則聲明(或通過基於@Transactional注解的方式),便可以將事務規則應用到業務邏輯中。

顯然聲明式事務管理要優於編程式事務管理,這正是spring倡導的非侵入式的開發方式。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上注解就可以獲得完全的事務支持。和編程式事務相比,聲明式事務唯一不足地方是,后者的最細粒度只能作用到方法級別,無法做到像編程式事務那樣可以作用到代碼塊級別。但是即便有這樣的需求,也存在很多變通的方法,比如,可以將需要進行事務管理的代碼塊獨立為方法等等。

聲明式事務管理也有兩種常用的方式,一種是基於tx和aop名字空間的xml配置文件,另一種就是基於@Transactional注解。顯然基於注解的方式更簡單易用,更清爽。

默認情況下,數據庫處於自動提交模式。每一條語句處於一個單獨的事務中,在這條語句執行完畢時,如果執行成功則隱式的提交事務,如果
執行失敗則隱式的回滾事務。

對於正常的事務管理,是一組相關的操作處於一個事務之中,因此必須關閉數據庫的自動提交模式。不過,spring會將底層連接的自動提交特性設置為false。
連接關閉時默認的策略是回滾任何未提交的事務

MyBatis自動參與到spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數據源與DataSourceTransactionManager引用的數據源一致即可,否則事務管理會不起作用。

spring事務特性

spring所有的事務管理策略類都繼承自org.springframework.transaction.PlatformTransactionManager接口

public interface PlatformTransactionManager {
 
  TransactionStatus getTransaction(TransactionDefinition definition)
    throws TransactionException;
 
  void commit(TransactionStatus status) throws TransactionException;
 
  void rollback(TransactionStatus status) throws TransactionException;
}

其中TransactionDefinition接口定義以下特性:

事務隔離級別

隔離級別是指若干個並發的事務之間的隔離程度。TransactionDefinition 接口中定義了五個表示隔離級別的常量:
•TransactionDefinition.ISOLATION_DEFAULT:這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
•TransactionDefinition.ISOLATION_READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀,不可重復讀和幻讀,因此很少使用該隔離級別。比如PostgreSQL實際上並沒有此級別。
•TransactionDefinition.ISOLATION_READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的數據。該級別可以防止臟讀,這也是大多數情況下的推薦值。
•TransactionDefinition.ISOLATION_REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,並且每次返回的記錄都相同。該級別可以防止臟讀和不可重復讀。
•TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。

事務傳播行為

所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。在TransactionDefinition定義中包括了如下幾個表示傳播行為的常量:
•TransactionDefinition.PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。這是默認值。
•TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。
•TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
•TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
•TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
•TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
•TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。

事務超時

所謂事務超時,就是指一個事務所允許執行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務。在 TransactionDefinition 中以 int 的值來表示超時時間,其單位是秒。
默認設置為底層事務系統的超時值,如果底層數據庫事務系統沒有設置超時值,那么就是none,沒有超時限制。

事務只讀屬性

只讀事務用於客戶代碼只讀但不修改數據的情形,只讀事務用於特定情景下的優化,比如使用Hibernate的時候。
默認為讀寫事務。

spring事務回滾規則

指示spring事務管理器回滾一個事務的推薦方法是在當前事務的上下文內拋出異常。spring事務管理器會捕捉任何未處理的異常,然后依據規則決定是否回滾拋出異常的事務。

默認配置下,spring只有在拋出的異常為運行時unchecked異常時才回滾該事務,也就是拋出的異常為RuntimeException的子類(Errors也會導致事務回滾),而拋出checked異常則不會導致事務回滾。
可以明確的配置在拋出那些異常時回滾事務,包括checked異常。也可以明確定義那些異常拋出時不回滾事務。

還可以編程性的通過setRollbackOnly()方法來指示一個事務必須回滾,在調用完setRollbackOnly()后你所能執行的唯一操作就是回滾。

@Transactional注解

屬性 類型 描述
value String 可選的限定描述符,指定使用的事務管理器
propagation enum: Propagation 可選的事務傳播行為設置
isolation enum: Isolation 可選的事務隔離級別設置
readOnly boolean 讀寫或只讀事務,默認讀寫
timeout int (in seconds granularity) 事務超時時間設置
rollbackFor Class對象數組,必須繼承自Throwable 導致事務回滾的異常類數組
rollbackForClassName 類名數組,必須繼承自Throwable 導致事務回滾的異常類名字數組
noRollbackFor Class對象數組,必須繼承自Throwable 不會導致事務回滾的異常類數組
noRollbackForClassName 類名數組,必須繼承自Throwable 不會導致事務回滾的異常類名字數組

@Transactional 可以作用於接口、接口方法、類以及類方法上。當作用於類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標注來覆蓋類級別的定義。
雖然 @Transactional 注解可以作用於接口、接口方法、類以及類方法上,但是 Spring 建議不要在接口或者接口方法上使用該注解,因為這只有在使用基於接口的代理時它才會生效。另外, @Transactional 注解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果你在 protected、private 或者默認可見性的方法上使用 @Transactional 注解,這將被忽略,也不會拋出任何異常。

第二部分 快速開始

2.1 在intellij idea中快速生成測試代碼

  1. 將鼠標放到類的任意位置,摁下Ctrl+Shift+T,然后Create a new Test即可。

第三部分 原理

JUnit4為了保證每個測試方法都是單元測試,是獨立的互不影響。所以每個測試方法執行前都會重新實例化測試類。

3.1 為什么Junit沒有main()方法就能運行

Junit4可以直接運行我們的某個方法,沒有main入口函數是斷然不行的。其實在org.junit.runner包下,有個JUnitCore.class,其中就有一個 標准的main方法,這就是JUnit入口函數。

Runner只是一個抽象類,表示用於運行Junit測試用例的工具,通過它可以運行測試並通知Notifier運行的結果。通常我們可以在待測方法所在的類之上使用@RunWith注解來為這個測試類指定一個特定的Runner。Junit的默認Runnner------BlockJunit4ClassRunner。當我們不為測試類添加@RunWith注解的時候,其實使用的就是這個Runner,它作為默認Runner只為我們提供了基本的基於Junit生命周期的測試注解。下面列出一些比較有用的Runner。

  1. Suit------它可以一次生執行全面在多個類中的測試用例,例如:
@RunWith(Suite.class)
@SuiteClasses({Person.class, People.class})
public class TestSuitMain{
  //雖然這個類是空的,但依然可以運行Junit測試,運行時,它會將Person.class和//People.class中的所有測試用命都執行一遍!
}
  1. Parameterized------在普通的單元測試中被@Test注解標注的測試方法只能是public void的,且不能有任何輸入參數。而這時常會給我們造成困擾,因為有時候我們需要為測試方法輸入參數,甚至是批量指定多個待測參數。這時Parameterized這個Runner就能滿足我們的要求,用法如下:
@RunWith(Parameterized.class)
public class TestGenerateParams{
    private String greeting;
    public TestGenerateParams(String greeting){
        super();
        this.greeting = greeting;
    }
    @Test
    public void testParams(){       
        System.out.println(greeting);
    }

    /**
     * 這里的返回的應該是一個可迭代數組,且方法必須是public static
     * @return
     */
    @Parameters
    public static List getParams(){
        return Arrays.asList(new String[][]{{"hello"},{"hi"},{"good morning"},{"how are you"}});
    }
}
  1. Theories------提供一組參數的排列組合值作為待測方法的輸入參數。同時注意到在使用Theories這個Runner的時候,我們的待測方法可以擁有輸入參數,而這在其它的Runner中的測試方法是不成的。下面是一個例子:
@RunWith(Theories.class)public class TheoriesTest{
    @DataPoint
    public static String nameValue1 = "Tony";
    @DataPoint
    public static String nameValue2 = "Jim";
    @DataPoint    public static int ageValue1 = 10;
    @DataPoint
    public static int ageValue2 = 20;
    @Theory
    public void testMethod(String name, int age){
        System.out.println(String.format("%s's age is %s", name, age));
    }
}

上面的代碼的意思是,將”Tony”、”Jim”、10、20四個參數以類型合法的排列組合傳給待沒方法。因此輸出的結果必然也有2x2=4種:

    Tony's age is 10 

    Tony's age is 20 

    Jim's age is 10 

    Jim's age is 20 

不過,為了簡單,我們除了可以使用@DataPoint注解來提供參數之外,還可以通過@DataPoints注解來提供參數,參照上述代碼,只需要將@DataPoint注解標注的四個字段參數替換為如下的兩個即可:

@DataPoints
public static String[] names = {"Tony", "Jim"};
@DataPoints
public static int[] ageValue1 = {10, 20};

3.2 基本過程

首先明確概念:
1.TestCase
代表一個測試用例,每一個TestCase實例都對應一個測試,這個測試通過這個TestCase實例的名字標志,以便在測試結果中指明哪個測試出現了問題。
即每個@Test注解的方法分別實例化,而非每個@RunWith注解的類

2.TestSuite
代表需要測試的一組測試用例。

3.TestFixtrue
TestFixtrue代表一個測試環境。它用於組合一組測試用例,這組測試用例需要共同的測試運行環境。

過程:

初始化階段(創建 Testcase 及 TestSuite)
首先創建一個 TestRunner 實例

public static void main (String[] args) {
  junit.textui.TestRunner.run (suite());
 }

然后,構造TestSuite:
TestSuite 采用了Composite 設計模式。在該模式下,可以將 TestSuite 比作一棵樹,樹中可以包含子樹(其它 TestSuite),也可以包含葉子 (TestCase),以此向下遞歸,直到底層全部落實到葉子為止。
然后將待測試的類(class文件)作為參數傳入TestSuite() 方法, TestSuite(Class theclass) 方法為 TestSuite 類的構造方法,它能自動分析 theclass 所描述的類的內部有哪些方法需要測試,並利用反射轉化為TestCase對象(注意每一個TestCase都是待測試類的一次重新實例化,故互不影響,即:一個TestCase類中可以定義很多test方法,但一個TestCase實例只對應一個測試方法。),加入到新構造的 TestSuite 中。

運行階段(運行所有的TestCase
對 TestSuite 中的整個“樹結構”遞歸遍歷運行其中的節點和葉子。

結果捕捉階段
運行測試的結果在TestResult實例中記錄,所以我們拋出Assert中的異常時,不會影響下面的測試繼續運行。

3.3 Spring測試框架+junit4單元測試原理

Spring的主要測試框架的核心是TestContext,TestContextManager,TestExcutionListener接口,我們每次啟動測試的時候都會創建TestContextManager,它實際上是管理了一個TestContext來負責持有一個當前測試的上下文,可以實現測試實例的依賴注入。TestContextManager還負責在測試中更新TestContext的狀態並代理到TestExecutionListener,它是用來監控實際的執行(如依賴注入,管理實務等等)。

@RunWith(SpringJUnit4ClassRunner.class)  //使用junit4進行測試  
@ContextConfiguration   ({"/spring/app*.xml","/spring/service/app*.xml"}) //加載配置文件  
@Transactional  
...

第四部分 常見問題

4.1 同一個測試類內部或者不同測試類之間的@Test執行順序

JUnit4.11之后提供了MethodSorters,在測試類上加注解@FixMethodOrder(value)可以有三種方式對test執行順序進行指定,如下:
默認(MethodSorters.DEFAULT),按方法名(MethodSorters.NAME_ASCENDING)和JVM(MethodSorters.JVM)

  • 默認順序由方法名hashcode值來決定,如果hash值大小一致,則按名字的字典順序確定,不同操作系統可能順序不同;
  • 按方法名稱的進行排序,由於是按字符的字典順序,所以以這種方式指定執行順序會始終保持一致; 不過這種方式需要對測試方法有一定的命名規則,如 測試方法均以testNNN開頭(NNN表示測試方法序列號 001-999)
    單元測試的目的就是測試最小單位的正確性,隔離和其他部分的關聯,自然也不能有依賴,不然,一定測試通不過,你無法知道是單元內部的問題,還是外部環境的問題。所以我們僅僅在blog表的測試中使用了這種排序規則
  • 按JVM返回的方法名的順序執行,此種方式下測試方法的執行順序是不可預測的,即每次運行的順序可能都不一樣(JDK7里尤其如此).

實際上 Junit里是通過反射機制得到某個Junit里的所有測試方法,並生成一個方法的數組,然后依次執行數組里的這些測試方法;
而當用annotation指定了執行順序,Junit在得到測試方法的數組后,會根據指定的順序對數組里的方法進行排序;

4.2 不同的測試類之間有重復的操作,如何保證測試數據不互相影響

由於Junit4不同測試(即每一個@Test都是一個單獨的單元測試,每個測試方法執行前都會重新實例化測試類)的默認執行順序是按照方法名的hash值排序,沒有並行測試。
所以可以用@Transactional 注解每個測試類,測試類內部如果沒有設置事務,則默認和類相同。那么在測試中,只要我們不提交事務,Spring默認會測試完畢回滾,因此不同的測試單元之前數據互不影響。
特別注意:在test中,Spring默認測試結束就會回滾,如果不想回滾,可以用@Rollback(false)注解;
而在一般的Java類中,Spring默認只有在拋出的異常為運行時unchecked異常時才回滾該事務,也就是拋出的異常為RuntimeException的子類(Errors也會導致事務回滾),而拋出checked異常則不會導致事務回滾,我們可以用@Transactional注解的rollbackFor屬性設置其他的

4.3 DAO層的測試一般insert在最前面,delete在最后,不同的測試單元之間數據需要互相使用,怎么辦?

解決1(不推薦):利用@FixMethodOrder(MethodSorters.NAME_ASCENDING)注解設定按照方法名字典順序執行測試,可以按照下面的命名方式:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyBlogApplication.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class BlogMapperTest1 {
    @Autowired
    private BlogMapper blogMapper;

    @Autowired
    private UserMapper userMapper;

    @Test
    public void test1InsertBlog() throws Exception {

    }

    @Test
    public void test2SelectBlogByUserUuid() throws Exception {
      
    }

    @Test
    public void test3DeleteBlogByBlogUuid() throws Exception {
    }
}

解決2: 每個單元測試都重新構造數據。。。當增刪改查很多時,為了保證測試類的清晰,推薦這種方法。

解決3: 把你需要共享數據所有操作放到一個@Test注解的方法中,比較適合操作比較少的測試。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM