Original: JUnit a cook's tour See www.junit.org
1 Introduction
2, goal
3, JUnit Design
3.1, starting from test case TestCase
3.2, fill in the method body in the Run () method
3.3, report results with TestResult object
3.4, No Stupid Subclasses - Testcase Again
3.5, don't worry is a test case or many test cases - Testsuite
3.6, summary
4, conclusions 1. In the Test Infected: Programmers Love Writing Tests, we describe how to write repeatable tests with a simple framework; this article describes how this framework is constructed. Learn the JUnit framework carefully, you can see how we designed this framework. We see different levels of JUnit tutorials, but in this article, we hope to explain more clearly. It is very valuable to figure out the design ideas of JUnit. Let's discuss JUnit's goals, these goals will be reflected in each of JUnit. We give the design and implementation of the JUnit framework around JUnit. We will describe this design with modes and program implementations. We will also see that there is of course other options when developing this framework. 2, what is the target of JUnit? First, we return to the premise assumptions. We assume that if a program can't automatically test, then it will not work. But there are more assumptions that if the developer guarantees the program to work, then it will always work properly, compared with this hypothesis, our hypothesis is too conservative. From this point of view, the developers wrote the code and debugged, and he could not say that his work was completed. He must prepare the test script and prove that the program is working properly. However, everyone is very busy, there is no time to test work. They will say that the time I write program code is very tight, how is it going to write test code? Therefore, the primary goal is to build a test framework. In this framework, developers can write test code. The framework can be used to use a familiar tool, you can master a lot of energy. It also eliminates unnecessary code, in addition to the necessary test code, eliminate duplicate labor. If you are just testing, you can implement it in the debugger. However, the test is not just this. Although your program is working very well, this is not enough, because you can't guarantee whether your program will be normal within a minute, you can't guarantee whether it is still normal in 5 years, then you have left a long time. Therefore, the second goal of the test is to create a test, and can retain these tests, and they are also valuable in the future, and other people can perform these tests and verify the test results. It is possible to collect different people's tests together, do it together, and don't worry about interference between them. Finally, you must also create new tests with existing tests. Each time you create a new test setting or Test fixture is costly, the framework can reuse the test settings, perform different tests. 3. The earliest design of JUnit, JUnit's design ideas originated in "patterns generate architectures". Its thought is that from 0 to design a system, one place application mode until the architecture of this system is constructed, so that a system is designed. We immediately propose the architecture issues to be resolved, using mode to solve this problem and explain how to apply these modes in JUnit. 3.1 From the test case TestCase first we create an object to represent the basics: Test Case.
Test cases often exist in the developer's minds, they use different ways to implement test cases: · Print statement · Debug expression · Test script how we think it is easy to manipulate test, then you must use the test as an object. The test in the brain of the developer is blurred, and the test is used as an object, which makes the test more specifically, the test can reserve long-term retention so that it is one of the targets of the test framework. At the same time, the object developers are accustomed to the object, so the test can be used as an object to achieve more attractive purpose of writing test code. Here, Command Mode satisfies our needs. This mode encapsulates the request to the object, which generates an object for the requesting operation, and there is an "execute" method in this object. In command mode, the requester is not calling the command executor, but through a command object to call the executive, specifically, first generate a command object for the command, and then dynamically set the command executor in this command object, last Call the command executor with the Execute method of the command object. This is a TestCase class definition code: [The translator has added] public abstract class testcase imports test {...} Because we want to reuse this class through inheritance, I am defined as "public abstract". Now let's implement the Test interface, in the design at this time, you just want to see TestCase as a single class. Each TestCase has a name attribute that can be used to identify which test case is used when the test fails. Public Abstract Class TestCase Implements Test {Private Final String FNAME; Public TestCase (String Name) {FNAME = Name;} PUBLIC Abstract Void Run (); ...} In order to illustrate the evolutionary process of JUnit, we use the figure to represent the architecture of each design phase. . We use simple symbols, gray road sign symbols to indicate the model used. When the role in this class is obvious, only the mode name is specified in the road sign; if the role in this class is not clear, the corresponding participation mode is also indicated in the road sign. This road sign symbol avoids confusion, see Figure 1. Figure 1 TestCase class Apply command mode 3.2, the problem to be solved under the Run () method is to give a convenient place to place the developer to place the test code and test code. TestCase is defined as abstract, indicating that developers should inherit TestCase to create their own test cases. If we are like just now, only in TestCase is placed in TestCase, there is no way, then the first goal is easy to write the test. This goal is difficult to achieve. For all tests, there is a universal structure. In this structure, the test jaws can be set, run some code under the test plug, check the run results, and then clear the test jaws. This shows that each test runs under different jaws, and the results of a test do not affect other test results, which meets the value maximization of the value of the test framework. Template Method Model is well solved the problem mentioned above. The intent of the template method mode is that the skeleton of the operation of an algorithm is defined in the parent class, and the specific step is delayed into the subclass.
The template method redefines the specific steps of an algorithm in the subclass without changing the structure of this algorithm, which is just our request. We only ask developers to know how to write fixture (ie Setup and Teardown) code, know how to write test code. Fixtue code and test code execution order are the same, regardless of how FixTure code and test code are written. This is the template method we need: public void run () {setup (); runtest (); Teardown ();} The default implementation of this template method is nothing. Protected void ruidest () {} protected void setup ()} Protected Void Teardown () {} Since the Setup and Teardown methods can be overwritten, it is also possible to be called by the framework, thus defining it. This stage is shown in Figure 2. Figure 2 TestCase.Run () method Apply template mode 3.3, report the result with TestResult object If a testcase is running in the original forest, probably no one cares about its test results. Your run test is to get a test record, indicating what testing, what is nothing. If a test success and failure opportunity is the same, or we only run a test, then we only use a flag in the test, check this flag after the test is completed. However, test success and failure opportunities are unbalanced, and testing is usually successful, so we only focus on test failures, and we only make one summary for successful records. In SmallTalk Best Practice Patter, there is a mode called "Collecting Parameter" mode. When you need to collect results in multiple methods, you can pass a parameter or object to the method, collect these methods with this object. Results of the. We create a new object, test result (TestResult), to collect the results of the test. Public class test () {frUntests = 0;}} This is a simple TestResult version, which is just counting the number of tests. In order to use TestResult, we must pass it as a parameter to the TestCase.Run () method, and notify TestResult The current test has begun. Public void Run (TestResult Result) {Result.StartTest (this); // Notice TestResult Test Start Setup (); Runtest (); Teardown ();} TestResult How many tests running tracking count: Public Synchronized Void Starttest (Test " Test) {frUnsests ;} We define the StartTest method in TestResult into synchronization, that is, thread secure, then a TestResult object can collect results in different threads. We want TestCase's interface to be simple, so we created a non-parameter version of the Run () method that created its own TestResult object.
Public TestResult Run () {TestResult Result = Createresult (); Run (Result); Return Result;} protected test;} protected test;}) () {Return New TestResult ();} The design used here is shown in Figure 3. Figure 3: TestResult Application Collection Parameter Mode If the test has been running correctly, then we don't have to write test. We are interested in testing, especially those we have not expected. Of course, we can expect the fault to appear in the way we hope, such as calculating an incorrect result, or a more strange fault approach, such as writing an array of offshore errors. Regardless of the test, we have to continue its subsequent test. JUnit distinguishes between Failure and Error (Error). The failure is expected, using assertions to detect, error is unpredictable, such as arrayindexoutofboundsexception. The fault identifies as an AssertionFaileDError error. In order to distinguish the unpredictable error from the fault, the fault is captured with the first CATCH statement, and the error outside the fault is captured with the second CATCH statement, which ensures that other tests after this test can be run. Public void Run (TestResult Result) {Result.StartTest (this); set {runtest ();} catch (assertionfailerror e) {// 1 result.addfailure (this, e);} catch (throwable e) {// 2 result.adderror (this, e);} finally {teardown ();}} AssertionFaileDError fault is triggered by the Assert method provided by TestCase. JUnit provides many Assert methods for different purposes, here there is a simple example: protected void assert (boolean condition) {if (! Condition) throw new assertionFaileDError ();} AssertionFaileDError failure is not a test customer (testing requester, Test method in TestCase is captured, but caught within TestCase.Run () in the template method. AssertionFaileDerror inherits from Error.
public class AssertionFailedError extends Error {public AssertionFailedError () {}} collection errors in TestResult follows: public synchronized void addError (Test test, Throwable t) {fErrors.addElement (new TestFailure (test, t));} public synchronized Void AddFailure (Test Test, Throwable T) {FFailures.addelement (New TestFailure (TEST, T));} TestFail is an internal help class, which will not succeed in testing and the exception occurred in its run. In order to report in the future. Public Class Testfailure Extends Object {Protected Test FFAILEDTEST; Protected THrowable FthrowNexception;} The collection parameter requires that it passes it to each method. If we do this, each test method requires a TestResult as a parameter, which will cause the signature structure of the test method to be destroyed; use exception, we can avoid signing the signed structure, this is also a use of side effects on exceptions. . Test case method, or the help method of the test case call to throw an exception, it doesn't have to know the TestResult information. Test methods in MoneytestSuite can be used as an example, which indicates that test methods do not have to know any information for TestResult. Public void testmoneyequals () {assert (! f12chf.equals (null)); Assertequals (F12CHF, F12CHF); Assertequals (F12CHF, New Money (12, "CHF")); assert (! f12chf.equals (f14chf)); } There are many different TestResult implementations in JUnit. The default implementation is simple, and it counts the number of faults and errors and collects results. TextTestResult represents the result of the collected results, while the JUnit test runner uses the UITESTRESULT, expressed the result of the collection with the graphical interface. TestResult is an extension point of the JUnit framework. Customers can define their own TestResult classes, for example, define an HTMLTESTRESULT class to report test results in the form of an HTML document. 3.4, no stupid subclasses - TestCase Again We apply command mode to represent a test. Command execution relies on one such method: Execute (), in TestCase called Run (), which makes the command to be called, which makes we use this same interface to achieve different commands. We need a universal interface to run our test. However, all test cases may be implemented in a class in a class, which avoids creating a class for each test method, resulting in a sharp increase in the number of classes. A complex test case class may implement many different test methods, each of which defines a simple test case. Each simple test case method is like this: TestMoneyEquals or TestMoneyAdd, the test case does not need to comply with the simple command mode interface, and different instances of the same Command class can call different test methods.
Therefore, the next question is that in the eyes of testing the customer (the caller) of the test, let all test cases look like. Review, this problem is solved by the design mode, we think of Adapter mode. The intent of the Adapter mode is to transform an existing interface to the interface required by the customer. This is in line with our needs, and Adapter has several different ways to do this. One way is the class adapter, which is the use of subclasses to adapt the interface. Specifically, inheriting the existing class with a subclass, constructing the new customers needed in an existing class. method. For example, to adapt TestMoneyEquals to RUNTEST, we inherit the Moneytest class, overwrite the RUNTEST method, this method calls the TestMoneyEquals method. Public Class TestMoneyEquals Extends Moneytest {Public TestMoneyequals () {Super ("TestMoneyEquals");} protected void runtest ()}}} The use of subclass adaptation requires a subclass for each test case, this Added the bulk burden. One goal of the JUnit frame is that it is as easy as possible when adding a use case. In addition, you can create a subclass for each test method, which will cause the class to expand. If there are many classes, there is one method in these classes, which is not worth it, which is difficult to take meaningful names. Java provides anonymous implicit mechanism to solve naming problems. We use anonymous hidden category to reach adapter purpose, without naming: testcase test = new Moneytest ("testmoneyequals") {protected void ruswest () {testmoneyequals ();}}; this is more convenient than usual subclasses. It still checks the type check at compile, and the cost is increasing the burden on the developer. SMALLTALK BEST PRACTICE PATTERNS describes another solution for this problem, and different instances are different in the same PLUGGABLE BEHAVIOR. Its idea is to use a class, which can be parameterized, ie performing different logic according to different parameter values, so that subclass inheritance is avoided. The simplest form of pluggable behavior is the pluggable selector. In SmallTalk, the Pluggable Selector is a variable that points to a method, which is a method pointer. This idea is not limited to SMALLTALK, and it is also suitable for Java. There is no way to select subsets in Java, however, Java's reflection (REFLECTION) API can call the method based on the method name this string, and we can use Java's reflection characteristics to implement the pluggable selector. Usually we rarely use Java reflection, here, we have to involve a bottom structure frame, which achieves reflection. JUnit provides two options to test customers: or use the pluggable selector, or use anonymous. By default, we use the Pluggable Selector mode, that is, the RunTest method.
In this way, the name of the test case must be consistent with the name of the test method. As shown below, we call methods with reflection characteristics. First, we look at the method object. Once there is this method object, we can pass it to its parameters and call it. Since our test method does not have parameters, we pass an empty parameter array: protected void runtest () throws throwable {method RunMethod = null; trylerthod = NULL; try {runmethod = getClass (). GetMethod (FName, New class [0] );} Catch ("Method /" FNAME "/" not found ", false);} Try {runmethod.invoke (this, new class [0]);} // catch invocationtargetexception and ILLEGALACCESSEXCEPTION} JDK1.1 Reflection API only lets us find the public method, so you must define the test method as public, otherwise you will get the NosuchMethodeXception exception. This is the design, Adapter mode, and PLUGGABLE SELECTOR mode. Figure 4: TestCase Application Adapter mode (anonymous) and PLUGGABLE SELECTOR Mode [Begin Translator Add] Due to only one RunTest method in TestCase, then it is only a test method in a testcase? Introduce the Pluggable Selector mode for this. Place multiple methods called Testxxxx () in TestCase, when new TestCase, specifies which TestXxx method with the template method Runtest with the template method Runtest with the template method. [END translator added] 3.5, don't worry is a test case or many test cases - TestSuite a system usually runs many tests. Now, JUnit can run a test and use the testResult report results. The next step is to extend JUnit so that it can run many different tests. If the caller doesn't care if it is running a test or a lot of tests, it runs a test and runs a lot of tests in the same way, then this problem is solved. Composite mode can solve this problem, its intention is that many objects constitute a tree-shaped structure, and Composite allows customers to handle individual objects and overall combination objects with the same interface. Part of the overall hierarchical structure is very meaningful, and a combined test may be made of many small combined tests, and small combined tests may be made of a single simple test. Composite models have the following participants: · Component: is a public unified interface for interaction with testing, regardless of this test is a simple test or combined test. · Composite: The interface used to maintain the test set, this test set is a combination test. · Leaf: indicates a simple test case, follow the Component interface.
This mode requires us to introduce an abstract class that defines a unified interface for simple objects and combinations objects. Its main role is to define this interface. In Java, we use the interface directly, there is no need to define the interface with an abstract class. Because Java has the concept of interface, while the concept of C has no interface, use the interface to avoid delivery of the JUnit function to a particular base class. All tests must follow this interface, so the test customer see is this interface: public interface test {public Abstract void Run (TestResult result);} The simple TestCase represented by Leaf realizes this interface, we have already discussed in front of us. . Below, we discuss Composite, which is a combination test case, called a test kit (TestSuite). Testsuite uses vector to store his child (Child Class Testsuite Implements Test {Private Vector Ftests = New Vector);} The Run () method of the test suite enters the child, ie calling its child's Run () Method: Public void Run (TestResult Result) {for (ENUMERATION E = ftests.elements (); E.hasMoreElements ();) {Test Test = (TEST) E.NEXTELEMENT (); test.run (result); }}} Figure 5: Test Suite Application Composite Mode Test Customer To add a test to the test suite, call the AddTest method: public void addtest (test) {ftests.addelement (TEST);} How to pay Test interface. Since TestCase and TestSuite follow the same TEST interface, the test suite can be recursively containing test cases and test kits. Developers can create their own TestSuite and run all of them with this suite. This is an example of creating TestSuite: public static test suite () {testsuite suite = new testsuite (); suite.addtest (New Moneytest ("TestMoneyEquals"); suite.addtest ("testsimpleadd");} [BeGin is helpful, here is the translator added] SUITE.ADDTEST (New MoneyEquals ") indicates that adding a test to the test suite suite, specifying the test class as Moneytest, the test method is TestMoneyequals (selected by Selector, docking with template method Runtest). In the MoneyTest class, there is no declaring MoneyTest (String) constructor, then MoneyTest ("TestMoneyEquals") is executed when the super (string) constructor is called, which is defined in the MoneyTest's parent TestCase.
Testcase (here, Moneytest) put "TestMoneYequals" string in private variables, this variable is a method pointer, using the PLUGGABLE Selector mode, indicating that the method it specifies TestMoneyEquals to interface with the template method Runtest. It indicates that the test case example is TestMoneyEquals (), and the reclaimed characteristics of Java will be used to achieve calls to this method. Therefore, the above code adds two test instances to Suite, and the type is Moneytest, but the test method is different. [End is helpful, here is the translator added] This example works well, but we want to add all the tests manually, this is a stupid way, when you write a test case, you have to remember Add to a static method Suite (), otherwise it will not run. To this end, we add a constructor for TestSuite, which uses the test case as its parameters, its role is to extract all the test methods in this class, and create a test suite to put these extracted test methods. In the test suite created. However, these test methods should comply with a simple agreement, which is named "TEST" as a prefix, and without parameters. This constructor uses this agreement to identify test methods using Java reflection characteristics and build test objects. If you use this constructor, the above code is very simple: public static test suite () {Return New TestSuite (MoneyTest.class);} is a test instance for each Testxxx method in the Moneytest class. [Add] but the translator is added] But the previous way is still useful, such as you only want to run a subset of test cases. 3.6, summary junit's design to this end. The figure below shows the mode used in the JUnit design. Figure 6: Mode in JUnit Note TestCase (the core function in the JUnit frame) is involved in 4 modes. This shows that in this framework, the TestCase class is "Pattern Density", which is the center of the framework, which has a strong association with other support characters. Here is another perspective to view the JUnit mode. In this plot, you see the effects of each pattern. Command mode creates a TestCase class, and the Template Method mode creates a RUN method, and so on. The symbols used here are from Figure 6, just remove the text. Figure 7: Mode plots in junit should pay attention to it, when we apply the Composite mode, complexity suddenly increases. Composite model is very powerful and useful. 4. Conclusion In order to draw conclusions, we do some general observations: Now, you are in an excellent situation to determine the model to describe whether the framework is valid. If you like the above discussion, then use this way to represent your system. · The mode intensity has a high mode intensity around TestCase, and TestCase is the key abstraction in the Junit design, it is easy to use, but it is difficult to change. We found that there is a high mode intensity around the key abstraction, which is the general phenomenon of mature framework. For immature frames, the situation is in contrary, they are not high. Once you find any questions you have to solve, you start "concentrating" your solution to achieve high mode intensiveness.