Junitソース分析


Junitは非常に短くて精悍なユニットテストフレームワークで、中には大量の設計モードと設計原則が使われています.もちろん、本文はこれらのモードを分析するのではなく、コードからその実行過程を分析するだけです.1.Junit 38 ClassRunnerの構造方法
        public JUnit38ClassRunner(Class<?> klass) {
                this(new TestSuite(klass.asSubclass(TestCase.class)));
        }

2.TestSuiteの構造方法  
public TestSuite(final Class<?> theClass) {
         addTestsFromTestCase(theClass);
   }

   次にaddTestsFromTestCase(theClass)メソッドのソースコードを見てみましょう  
private void addTestsFromTestCase(final Class<?> theClass) {
                fName= theClass.getName();
                try {
                        getTestConstructor(theClass); // Avoid generating multiple error messages
                } 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 : superClass.getDeclaredMethods())
                                addTestMethod(each, names, theClass);
                        superClass= superClass.getSuperclass();
                }
                if (fTests.size() == 0)
                        addTest(warning("No tests found in "+theClass.getName()));
        }

次にaddTestMethodメソッドを見てみましょう       
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("Test method isn't public: "+ m.getName() + "(" + theClass.getCanonicalName() + ")"));
                        return;
                }
                names.add(name);
                addTest(createTest(theClass, name));
        }

isTestMethod(Method m)メソッドは、このメソッドがテストメソッドであるか否かを判断する.すなわち、testの先頭または関連注釈のあるメソッドaddTest(createTest(theClass,name))である.ここではTestCaseのインスタンスを作成し、fTests(Vector)で作成したTestCaseのfnameプロパティに追加します.実際にテストするメソッド名3.TestSuiteのcreateTestメソッドが保存されます.       
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;
        }

次にTestの実行プロセスに入ります:4.Junit 38 ClassRunnerのrunメソッド  
public void run(RunNotifier notifier) {
         TestResult result= new TestResult();
         result.addListener(createAdaptingListener(notifier));
         getTest().run(result);
   }

   ここでgetTest()が得たのはTestSuiteで、上のコードはTestSuiteに複数のTestCaseインスタンスが含まれていることを知っています.もちろん、ここで言う比較限界TestSuiteは他のTestSuiteを含めてテストを実行することもできます.それらはfTestsに保存されています.これは実際に組み合わせモードです.では、TestSuiteのrun方法を見てみましょう.   5.TestSuiteのrunメソッド       
public void run(TestResult result) {
                for (Test each : fTests) {
                          if (result.shouldStop() )
                                  break;
                        runTest(each, result);
                }
        }

  予想外に、ここではfTestsを巡って各TestCaseインスタンスのrunメソッドを実行します.では、TestCaseのrunメソッドを見てみましょう.  6.TestCaseのrunメソッド       
public void run(TestResult result) {
                result.run(this);
        }

  ここでTestCaseは自分をパラメータとしてTestResultに渡すrunメソッドを実行しますが、ここではなぜこのようにするのか、なぜ直接実行しないのでしょうか.この問題はまずここに残って、それから見ます.  7.TestResultのrunメソッド       
protected void run(final TestCase test) {
                startTest(test);
                Protectable p= new Protectable() {
                        public void protect() throws Throwable {
                                test.runBare();
                        }
                };
                runProtected(test, p);

                endTest(test);
        }

  startTest、endTestメソッドは初期化と破棄の作業にほかならないので、各メソッドに入って見ないでください.TestResultはその名の通りテスト結果を収集するために使用され、Runner、TestSuite、TestCaseの間でずっと伝達され、すべてのテストメソッドの実行結果を保存します.  ここでは保護されたProtectableクラスを作成し、TestCaseのrunBareメソッドを実行します.runProtected()はp.protect()メソッドを呼び出します.これはテストメソッドが実際にTestCaseで実行されていることを示しています.では、なぜTestResultに渡され、TestResultクラスでTestCaseのrunBareメソッドを呼び出すのか、runProtected()の具体的な手順を見てみましょう    8.TestResultのrunProtectedメソッド       
public void runProtected(final Test test, Protectable p) {
                try {
                        p.protect();
                } 
                catch (AssertionFailedError e) {
                        addFailure(test, e);
                }
                catch (ThreadDeath e) { // don't catch ThreadDeath by accident
                        throw e;
                }
                catch (Throwable e) {
                        addError(test, e);
                }
        }

  明らかに、このプロセスは実行結果を収集するためのものであり、catchからAssertionFailureErrorまでのテスト結果が正しくない場合、catchから他の異常表示テストにエラーが発生した場合、addFailureおよびaddErrorメソッドは、実際にはTestResultのfFailuresおよびfErrorsリストにオブジェクトを追加し、対応する観察者に通知することである.  メイン・プロシージャに戻り、テスト・メソッドがどのように呼び出されたかを見てみましょう.  9.TestCaseのrunBare()メソッド       
public void runBare() throws Throwable {
                Throwable exception= null;
                setUp();
                try {
                        runTest();
                } catch (Throwable running) {
                        exception= running;
                }
                finally {
                        try {
                                tearDown();
                        } catch (Throwable tearingDown) {
                                if (exception == null) exception= tearingDown;
                        }
                }
                if (exception != null) throw exception;
        }

    ここでは、junitが各メソッドをテストする際にsetup()、実際にテストするメソッド、tearDown()メソッドを前後して実行する理由を発見します.ここで使用するテンプレートメソッドモードでは、setupとtearDownがテストクラスに複写されている場合、実際に実行されるのはテストクラスのsetupとtearDownであり、各クラスをテストする前後でこの2つのメソッドが実行されます.実際にテストを実行する方法はrunTest()で実現されていますが、runTest()の方法を見てみましょう  10.TestCaseのrunTest()メソッド       
protected void runTest() throws Throwable {
                assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);
                Method runMethod= null;
                try {
                        // use getMethod to get all public inherited
                        // methods. getDeclaredMethods returns all
                        // methods of this class but excludes the
                        // inherited ones.
                        runMethod= getClass().getMethod(fName, (Class[])null);
                } catch (NoSuchMethodException e) {
                        fail("Method \""+fName+"\" not found");
                }
                if (!Modifier.isPublic(runMethod.getModifiers())) {
                        fail("Method \""+fName+"\" should be public");
                }

                try {
                        runMethod.invoke(this);
                }
                catch (InvocationTargetException e) {
                        e.fillInStackTrace();
                        throw e.getTargetException();
                }
                catch (IllegalAccessException e) {
                        e.fillInStackTrace();
                        throw e;
                }
        }

   ここでJunitは実際に私たちがテストする方法を反射してinvokeに行くことがわかります.以上がJunitがテストする全過程です.次に、前の過程で残した問題を分析してみましょう.   TestCaseのrunメソッドではテストメソッドを直接実行せずにTestResultのrunメソッドに自分自身を転送して実行するのはなぜですか?   この問題は後の実行過程を通じて、みんなも理解したはずで、これは実はJunitの設計の上でとても私達の学習に値するところで、類の単一の職責の特徴で、TestCaseの中の方法はすべてテストに関連するべきで、TestResultの中の方法はすべて結果のためにサービスして、この織りなす過程はテストの結果を得るためです