1. 程式人生 > >junit原始碼解析--初始化階段

junit原始碼解析--初始化階段

OK,我們接著上篇整理。上篇部落格中已經列出的junit的幾個核心的類,這裡我們開始整理junit完整的生命週期。

  • JUnit 的完整生命週期分為 3 個階段:初始化階段、執行階段和結果捕捉階段。
這篇這裡先來整理下junit的初始化階段。也就是建立 Testcase 及 TestSuite。先來貼出junit測試框架入口:
/**
     * @建立時間: 2016年1月21日
     * @相關引數: @param args
     * @功能描述: 測試框架入口
     */
    public static void main(String[] args)
    {
        TestRunner aTestRunner = new TestRunner();
        try
        {
            //String[] linkinArgs = new String[] { "-m", "org.linkinpark.commons.textui.LinkinTest.testLinkin4Normal" };
            // String[] linkinArgs = new String[] { "-v", "-m",
            // "org.linkinpark.commons.textui.LinkinTest.testLinkin4Normal" };
            // String[] linkinArgs = new String[] { "-c", "org.linkinpark.commons.textui.LinkinTest" };
            // String[] linkinArgs = new String[] { "org.linkinpark.commons.textui.LinkinTest" };
            // String[] linkinArgs = new String[] { "-wait", "org.linkinpark.commons.textui.LinkinTest" };
            String[] linkinArgs = new String[] { "org.linkinpark.commons.textui.LinkinTestAll" };
            TestResult testResult = aTestRunner.start(linkinArgs);
            if (!testResult.wasSuccessful())
            {
                System.exit(FAILURE_EXIT);
            }
            System.exit(SUCCESS_EXIT);
        }
        catch (Exception e)
        {
            System.err.println(e.getMessage());
            System.exit(EXCEPTION_EXIT);
        }
    }

初始化階段作一些重要的初始化工作,它的入口點在 junit.textui.TestRunner 的 main 方法。該方法首先建立一個 TestRunner 例項 aTestRunner。之後 main 函式中主體工作函式為 TestResult r = aTestRunner.start(args) 。這裡貼出測試執行器runner的start方法。
/**
     * @建立時間: 2016年1月21日
     * @相關引數: @param args
     * @相關引數: @return
     * @相關引數: @throws Exception
     * @功能描述: 對命令列引數進行解析,進行測試
     * <p>
     * 引數:如果什麼都不傳,預設測試整個類
     * “ -wait ”:等待模式,測試完畢使用者手動返回。
     * “ -c ”:測試整個類。
     * “-m ”:測試單個方法。
     * “ -v ”:版本顯示。
     * </p>
     */
    public TestResult start(String[] args) throws Exception
    {
        String testCase = "";
        String method = "";
        boolean wait = false;
        for (int i = 0; i < args.length; i++)
        {
            if (args[i].equals("-wait"))
            {
                wait = true;
            }
            else if (args[i].equals("-c"))
            {
                testCase = extractClassName(args[++i]);
            }
            else if (args[i].equals("-m"))
            {
                String arg = args[++i];
                int lastIndex = arg.lastIndexOf('.');
                testCase = arg.substring(0, lastIndex);
                method = arg.substring(lastIndex + 1);
            }
            else if (args[i].equals("-v"))
            {
                System.err.println("linkin-frame-junit測試框架版本號==> " + Version.id());
            }
            else
            {
                testCase = args[i];
            }
        }


        if (StringUtils.isBlank(testCase))
        {
            throw new Exception("執行引數不能為空,親愛的!!!");
        }


        try
        {
            if (StringUtils.isNotBlank(method))
            {
                return runSingleMethod(testCase, method, wait);
            }
            Test suite = getTest(testCase);
            return doRun(suite, wait);
        }
        catch (Exception e)
        {
            throw new Exception("初始化測試執行器出錯: " + e);
        }
    }

關於start方法的幾個引數這裡不做贅述了,具體的看我程式碼上面的註釋。將測試類的全限定名解析後,開始構造測試元件TestSuite。通過getTest方法獲取測試元件TestSuite,然後開始doRun去執行測試了。關於執行測試用例我們下篇在說,這裡先來看junit初始化測試元件TestSuite這塊。我們先來看看測試執行器中獲取測試元件的這塊程式碼:

/**
	 * @建立時間: 2016年1月21日
	 * @相關引數: @param suiteClassName
	 * @相關引數: @return
	 * @功能描述: 模板方法,獲得suite中的測試用例
	 */
	public Test getTest(String suiteClassName)
	{
		if (suiteClassName.length() <= 0)
		{
			clearStatus();
			return null;
		}
		Class<?> testClass = null;
		try
		{
			testClass = loadSuiteClass(suiteClassName);
		}
		catch (ClassNotFoundException e)
		{
			String clazz = e.getMessage();
			if (clazz == null)
			{
				clazz = suiteClassName;
			}
			runFailed("Class not found \"" + clazz + "\"");
			return null;
		}
		catch (Exception e)
		{
			runFailed("Error: " + e.toString());
			return null;
		}
		// TestSuite 的構造分兩種情況
		Method suiteMethod = null;
		try
		{
			// 使用者在測試類中通過宣告 Suite() 方法自定義 TestSuite
			suiteMethod = testClass.getMethod(SUITE_METHODNAME);
		}
		catch (Exception e)
		{
			// try to extract a test suite automatically
			clearStatus();
			// 自動判斷並提取測試方法
			return new TestSuite(testClass);
		}
		if (!Modifier.isStatic(suiteMethod.getModifiers()))
		{
			runFailed("Suite()方法必須是靜態的呢");
			return null;
		}
		Test test = null;
		try
		{
			test = (Test) suiteMethod.invoke(null); // static method
			if (test == null)
			{
				return test;
			}
		}
		catch (InvocationTargetException e)
		{
			runFailed("Failed to invoke suite():" + e.getTargetException().toString());
			return null;
		}
		catch (IllegalAccessException e)
		{
			runFailed("Failed to invoke suite():" + e.toString());
			return null;
		}

		clearStatus();
		return test;
	}

TestSuite 的構造分兩種情況:

  • A:使用者在測試類中通過宣告 Suite() 方法自定義 TestSuite 。(try塊中)

  • B:JUnit 自動判斷並提取測試方法。(catch塊中)


1,A來解釋一下,JUnit 提供給使用者兩種構造測試集合的方法,使用者既可以自行編碼定義結構化的 TestCase 集合,也可以讓 JUnit 框架自動建立測試集合,這種設計融合其它功能,讓測試的構建、執行、反饋三個過程完全無縫一體化。

當suite方法在我們自己寫的測試類中定義時,JUnit 建立一個顯式的testSuite,它利用Java語言的 Reflection 機制找出名為SUITE_METHODNAME的方法,也即suite方法:

suiteMethod = testClass.getMethod(SUITE_METHODNAME);
Reflection 是 Java 的高階特徵之一,藉助 Reflection 的 API 能直接在程式碼中動態獲取到類的語言程式設計層面的資訊,如類所包含的所有的成員名、成員屬性、方法名以及方法屬性,而且還可以通過得到的方法物件,直接呼叫該方法。 JUnit 原始碼頻繁使用了 Reflection 機制,不僅充分發揮了 Java 語言在系統程式設計要求下的超凡能力,也使 JUnit 能在使用者自行編寫的測試類中游刃有餘地分析並提取各種屬性及程式碼,而其它測試框架需要付出極大的複雜性才能得到等價功能。
若 JUnit 無法找到 siute 方法,則丟擲異常,流程進入情況 B 程式碼;若找到,則對使用者提供的 suite 方法進行外部特徵檢驗,判斷是否為類方法。最後,JUnit 自動呼叫該方法,構造使用者指定的 TestSuite:
test = (Test) suiteMethod.invoke(null); // static method
這裡對上面這行程式碼解釋一下,反射中method的執行API如下:

=========================================================================================================================================================================================

invoke

對帶有指定引數的指定物件呼叫由此 Method 物件表示的底層方法。個別引數被自動解包,以便與基本形參相匹配,基本引數和引用引數都隨需服從方法呼叫轉換。

如果底層方法是靜態的,那麼可以忽略指定的 obj 引數。該引數可以為 null。

如果底層方法所需的形引數為 0,則所提供的 args 陣列長度可以為 0 或 null。

如果底層方法是例項方法,則使用動態方法查詢來呼叫它,這一點記錄在 Java Language Specification, Second Edition 的第 15.12.4.4 節中;在發生基於目標物件的執行時型別的重寫時更應該這樣做。

如果底層方法是靜態的,並且尚未初始化宣告此方法的類,則會將其初始化。

如果方法正常完成,則將該方法返回的值返回給呼叫者;如果該值為基本型別,則首先適當地將其包裝在物件中。但是,如果該值的型別為一組基本型別,則陣列元素 被包裝在物件中;換句話說,將返回基本型別的陣列。如果底層方法返回型別為 void,則該呼叫返回 null。

引數:
obj - 從中呼叫底層方法的物件
args - 用於方法呼叫的引數
返回:
使用引數 args 在 obj 上指派該物件所表示方法的結果
丟擲:
 - 如果此 Method 物件強制執行 Java 語言訪問控制,並且底層方法是不可訪問的。
 - 如果該方法是例項方法,且指定物件引數不是宣告底層方法的類或介面(或其中的子類或實現程式)的例項;如果實參和形參的數量不相同;如果基本引數的解包轉換失敗;如果在解包後,無法通過方法呼叫轉換將引數值轉換為相應的形參型別。
=========================================================================================================================================================================================

2,B來解釋一下,如果使用者沒有在自己的測試類中自定義suite方法,那麼系統將自動建立一個suite。
return new TestSuite(testClass);

我們現在來認真看下這裡,這裡是junit預設的測試元件的初始化。TestSuite構造過程程式碼如下:

private String fName; // 測試類的類名,注意,TestCase中的fName是方法名。
	private Vector<Test> fTests = new Vector<Test>(10); // 用來裝用例的,可以的是TestCase,也可以是TestSuite
	
	public TestSuite(final Class<?> theClass)
	{
		addTestsFromTestCase(theClass);
	}

	private void addTestsFromTestCase(final Class<?> theClass)
	{
		fName = theClass.getName();
		try
		{
			getTestConstructor(theClass);
		}
		catch (NoSuchMethodException e)
		{
			addTest(warning("Class " + theClass.getName() + " has no public constructor TestCase(String name) or TestCase()"));
			return;
		}

		if (!Modifier.isPublic(theClass.getModifiers()))
		{
			addTest(warning("Class " + theClass.getName() + " is not public"));
			return;
		}

		Class<?> superClass = theClass;
		List<String> names = new ArrayList<String>();
		while (Test.class.isAssignableFrom(superClass))
		{
			for (Method each : MethodSorter.getDeclaredMethods(superClass))
			{
				addTestMethod(each, names, theClass);
			}
			superClass = superClass.getSuperclass();
		}
		if (fTests.size() == 0)
		{
			addTest(warning(theClass.getName() + "沒有測試方法耶"));
		}
	}
	
	/**
	 * @建立時間: 2016年2月1日
	 * @相關引數: @param m
	 * @相關引數: @param names
	 * @相關引數: @param theClass
	 * @功能描述: 新增每個方法到方法List中去
	 */
	private void addTestMethod(Method m, List<String> names, Class<?> theClass)
	{
		String name = m.getName();
		if (names.contains(name))
		{
			return;
		}
		if (!isPublicTestMethod(m))
		{
			if (isTestMethod(m))
			{
				addTest(warning(m.getName() + "(" + theClass.getCanonicalName() + ")方法非public修飾"));
			}
			return;
		}
		names.add(name);
		addTest(createTest(theClass, name));
	}
	
	// 以下2個方法是junit測試方法的約定
	private boolean isPublicTestMethod(Method m)
	{
		return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
	}

	private boolean isTestMethod(Method m)
	{
		return m.getParameterTypes().length == 0 && m.getName().startsWith("test") && m.getReturnType().equals(Void.TYPE);
	}
	
	/**
	 * @建立時間: 2016年2月1日
	 * @相關引數: @param message
	 * @相關引數: @return
	 * @功能描述: 約定無效然後返回一個失敗的測試用例
	 */
	public static Test warning(final String message)
	{
		return new TestCase("warning")
		{
			@Override
			protected void runTest()
			{
				fail(message);
			}
		};
	}
	
	/**
	 * @建立時間: 2016年1月22日
	 * @相關引數: @param test
	 * @功能描述: 新增一個測試
	 */
	public TestSuite addTest(Test test)
	{
		fTests.add(test);
		return this;
	}

上面的程式碼我截取了部分原始碼,我們來挑選幾個重要的說明以下:

1),在TestSuite構造器中呼叫addTestsFromTestCase方法來初始化測試元件,在addTestsFromTestCase程式碼中有一大亮點:

while (Test.class.isAssignableFrom(superClass))
		{
			for (Method each : MethodSorter.getDeclaredMethods(superClass))
			{
				addTestMethod(each, names, theClass);
			}
			superClass = superClass.getSuperclass();
		}
TestSuite 採用了Composite 設計模式。在該模式下,可以將 TestSuite 比作一棵樹,樹中可以包含子樹(其它 TestSuite),也可以包含葉子 (TestCase),以此向下遞迴,直到底層全部落實到葉子為止。 JUnit 採用 Composite 模式維護測試集合的內部結構,使得所有分散的 TestCase 能夠統一集中到一個或若干個 TestSuite 中,同類的 TestCase 在樹中佔據同等的位置,便於統一執行處理。另外,採用這種結構使測試集合獲得了無限的擴充性,不需要重新構造測試集合,就能使新的 TestCase 不斷加入到集合中。

關於這裡while迴圈控制的條件,我們再來看下api,實際編碼中反射用的比較少,這個isAssignableFrom更是第一次遇見。

=========================================================================================================================================================================================

isAssignableFrom

public boolean isAssignableFrom(Class<?> cls)
判定此 Class 物件所表示的類或介面與指定的 Class 引數所表示的類或介面是否相同,或是否是其超類或超介面。如果是則返回 true;否則返回 false。如果該 Class 表示一個基本型別,且指定的 Class 引數正是該 Class 物件,則該方法返回 true;否則返回 false

特別地,通過身份轉換或擴充套件引用轉換,此方法能測試指定 Class 引數所表示的型別能否轉換為此 Class 物件所表示的型別。有關詳細資訊,請參閱 Java Language Specification 的第 5.1.1 和 5.1.4 節。

引數:
cls - 要檢查的 Class 物件
返回:
表明 cls 型別的物件能否賦予此類物件的 boolean 值
丟擲:

=========================================================================================================================================================================================

在 TestSuite 類的程式碼中,可以找到該類的2個屬性,

private String fName; // 測試類的類名,注意,TestCase中的fName是方法名。
	private Vector<Test> fTests = new Vector<Test>(10); // 用來裝用例的,可以的是TestCase,也可以是TestSuite
其中這個fTests此即為內部維護的“子樹或樹葉”的列表。前面的迴圈程式碼完成提取整個類繼承體系上的測試方法的提取。迴圈語句由Class型別的例項theClass開始,逐級向父類的繼承結構追溯,直到頂級Object類,並將沿途各級父類中所有合法的 testXXX() 方法都加入到 TestSuite中。在迭代過程中,如果方法list中已經包含該方法,則直接return。防止不同級別父類中的 testXXX() 方法重複加入 TestSuite 。
String name = m.getName();
		if (names.contains(name))
		{
			return;
		}
在每次的迭代中呼叫addTestMethod建立一個測試用例,新增到上面的fTests中去。下面貼出通過測試類class檔案和測試方法名來建立一個測試用例的程式碼:
/**
	 * @建立時間: 2016年2月1日
	 * @相關引數: @param theClass 測試類
	 * @相關引數: @param name 方法名
	 * @相關引數: @return
	 * @功能描述: 建立一個測試用例
	 */
	static public Test createTest(Class<?> theClass, String name)
	{
		Constructor<?> constructor;
		try
		{
			constructor = getTestConstructor(theClass);
		}
		catch (NoSuchMethodException e)
		{
			return warning("Class " + theClass.getName() + " has no public constructor TestCase(String name) or TestCase()");
		}
		Object test;
		try
		{
			if (constructor.getParameterTypes().length == 0)
			{
				test = constructor.newInstance(new Object[0]);
				if (test instanceof TestCase)
				{
					((TestCase) test).setName(name);
				}
			}
			else
			{
				test = constructor.newInstance(new Object[] { name });
			}
		}
		catch (InstantiationException e)
		{
			return (warning("Cannot instantiate test case: " + name + " (" + exceptionToString(e) + ")"));
		}
		catch (InvocationTargetException e)
		{
			return (warning("Exception in constructor: " + name + " (" + exceptionToString(e.getTargetException()) + ")"));
		}
		catch (IllegalAccessException e)
		{
			return (warning("Cannot access test case: " + name + " (" + exceptionToString(e) + ")"));
		}
		return (Test) test;
	}
	
	/**
	 * @建立時間: 2016年1月22日
	 * @相關引數: @param theClass
	 * @相關引數: @return
	 * @相關引數: @throws NoSuchMethodException
	 * @功能描述: 獲取測試類構造器,如果沒有形參是一個字串的構造器,則返回無參構造器
	 */
	public static Constructor<?> getTestConstructor(Class<?> theClass) throws NoSuchMethodException
	{
		try
		{
			return theClass.getConstructor(String.class);
		}
		catch (NoSuchMethodException e)
		{
		}
		return theClass.getConstructor();
	}
在這塊程式碼中,通過反射方法newInstance獲取一個測試用例,在反射的過程中也將每一個測試用例TestCase的fName設定成方法名字,注意createTest方法的那塊向下強轉的程式碼。這裡強調一個事情:

TestSuite中的fName是測試元件包含的每一個測試用例的名字,TestCase中的fName是每一個測試用例的包含的測試的名字,別搞混了。

/**
 * @建立作者: LinkinPark
 * @建立時間: 2016年1月21日
 * @功能描述: Test類的集合。
 * <p>
 * 1,如果自己的測試類中沒有指定suite()方法,TestSuite自動初始化。
 * TestSuite suite= new TestSuite(LinkinTest.class);
 * 2,動態往suite中新增test。此時注意:測試類要提供一個字串引數的的構造器,且super(method)
 * TestSuite suite= new TestSuite();
 * suite.addTest(new LinkinTest("testLinkin4Normal"));
 * suite.addTest(new LinkinTest("testLinkin8Error"));
 * 3,動態往suite中新增suite
 * TestSuite suite= new TestSuite();
 * suite.addTestSuite(LinkinTest.class);
 * 此建構函式建立一個所有方法的套件,套件裡面的每個方法以test開頭並且沒有任何引數。
 * 4,TestSuite還可以傳入一個數組
 * Class[] testClasses = { LinkinTest.class, LinkinTest1.class }
 * TestSuite suite= new TestSuite(testClasses);
 * </p>
 */
public class TestSuite implements Test
{
	private String fName; // 測試類的類名,注意,TestCase中的fName是方法名。
	private Vector<Test> fTests = new Vector<Test>(10); // 用來裝用例的,可以的是TestCase,也可以是TestSuite
}
public abstract class TestCase extends Assert implements Test
{
	private String fName; // 測試方法的名字
}

OK,至此已經將所有的TestSuite的初始化過程全部整理完了,在執行器TestRunner中初始化TestSuite結束後就開始執行測試用例了。
Test suite = getTest(testCase);
			return doRun(suite, wait);

下篇部落格我會整理Junit的測試用例階段。

最後這裡以命令者設計模式在junit中的應用整理結束這篇部落格。

以我們在往TestSuite中的fTest中新增測試用例的那塊程式碼為例,我們在校驗了每個測試方法是否遵循我們的測試約定之後,就開始往fTest中新增測試用例了。

addTest(createTest(theClass, name));
這行程式碼將 testXXX 方法轉化為 TestCase,並加入到 TestSuite 。其中,addTest 方法接受 Test 介面型別的引數,其內部有 countTestCases 方法和 run 方法,該介面被 TestSuite 和 TestCase 同時實現。這是 Command 設計模式精神的體現,
Command 模式將呼叫操作的物件與如何實現該操作的物件解耦。在執行時,TestCase 或 TestSuite 被當作 Test 命令物件,可以像一般物件那樣進行操作和擴充套件,也可以在實現 Composite 模式時將多個命令複合成一個命令。另外,增加新的命令十分容易,隔離了現有類的影響,今後,也可以與備忘錄模式結合,實現 undo 等高階功能。