Original article: JUnit a Cook's tour see www.junit.org
1. Introduction
2. Objectives
3. JUnit Design
3.1 starting from the test case testcase
3.2 fill in the method body in the run () method
3.3 report results using testresult objects
3.4. No stupid subclasses-testcase again
3.5 don't worry about a test case or many test cases-testsuite
3.6 Summary
4. Conclusion
1. Introduction
In an earlier article (test infected: programmers love writing tests), we described how to use a simple
This article describes how the framework is constructed.
By carefully studying the JUnit framework, we can see how we design this framework. We can see the JUnit tutorials at different levels,
But in this article, we hope to clarify the problem more clearly. Understanding the design ideas of JUnit is very valuable.
Let's first discuss JUnit's goals, which will be reflected in every detail of JUnit. Around JUnit's goal, I
The JUnit framework is designed and implemented. We will describe the design using the pattern and program implementation example. We can also see that
Of course, there are other options.
2. Objectives
What is JUnit's goal?
First, let's go back to the premise of development. Suppose that a program cannot be automatically tested, then it will not work. But there are more
Many assumptions that, if a developer ensures that the program can work, it will always work normally. Compared with this assumption, our false
It is too conservative.
From this point of view, the developer has compiled the code and debugged it. It cannot be said that his work has been completed, and he must compile the test.
Script to prove that the program works properly. However, everyone is very busy and has no time to perform the test. They will say that I write the program code
Is there time to write test code?
Therefore, the primary goal is to build a test framework in which developers can write test code. Use the framework
Familiar tools can be mastered without much effort. It also eliminates unnecessary code, in addition to the required test code
Labor.
If you only want to perform the test, you can write an expression in the debugger. However, testing is not just that.
Although your program works well, it is not enough because you cannot guarantee that your program will be normal within one minute after integration.
It cannot be guaranteed that it is still normal within five years, and you have been away for a long time.
Therefore, the second goal of the test is to create a test and retain the tests. They will also be valuable in the future. Others can
Perform these tests and verify the test results. If possible, we need to collect tests from different people and execute them together without worrying.
They interfere with each other.
Finally, you must use an existing test to create a new test. Each time a new test setting or test fixture is created
The framework can reuse the test settings and perform different tests.
3. JUnit Design
The earliest concept of JUnit was originated from the article "patterns generate generative tures. It's thinking
The idea is to design a system from 0 and apply the model one by one until the system architecture is constructed.
System design. We will immediately propose the architecture problem to be solved, use models to solve the problem, and explain how to apply these models in JUnit.
.
3.1 starting from the test case testcase
First, we create an object to represent the basic concept: Test Case (testcase ). Test Cases often exist in developers'
In their minds, they implement test cases in different ways:
· Print statement
· Debugging expression
· Test script
If we want to easily manipulate the test, we must take the test as an object. Testing is vague in the minds of developers.
This makes the test more specific, and the test can be retained for a long time for future use. This is one of the goals of the test framework. In addition
As developers get used to objects, taking tests as objects can make writing test code more attractive.
Here, the command mode meets our needs. In this mode, the request is encapsulated into an object, that is, a pair is generated for the request operation.
For example, this object has an "execute" method. In command mode, the requester does not directly call the command executor, but uses
Call the executor through a command object. Specifically, first generate a command object for the command request, and then dynamically
Set the command executor and use the execute method of the command object to call the command executor. This is the testcase class definition code:
Added 〕
Public abstract class testcase implements test {
...
}
Because we want to inherit and reuse this class, we define it as "public abstract ". Now let's ignore its implementation.
Test interface. In this design, you only need to regard testcase as a single class.
Each testcase has a name attribute. When a test fails, you can use it to identify the test case.
Public abstract class testcase implements test {
Private final string fname;
Public testcase (string name ){
Fname = Name;
}
Public abstract void run ();
...
}
To illustrate the evolution of JUnit, we use graphs to represent the architecture of each design stage. We use simple symbols, gray road signs
Indicates the mode used. When the role of this class in the mode is obvious, only the mode name is specified in the roadmap; if this class is in the Mode
If the role in is not clear, the corresponding Participation Mode is also indicated in the roadmap. This sign avoids confusion, as shown in figure 1.
Figure 1 command mode applied to the testcase class
3.2 fill in the method body in the run () method
The problem to be solved below is to provide a convenient place for developers to place the setup code and test code for testing.
Testcase is defined as abstract, indicating that developers must inherit testcase to create their own test cases. If we only
There is no way to place a variable in testcase, so the first goal is hard to achieve, that is, it is easy to compile the Test Generation.
For all tests, there is a general structure in which you can set the test clamp (fixture) under the test clamp
Run some code, check the running results, and then clear the test clamp. This indicates that each test runs under a different clamp, a test
The results will not affect other test results, which is consistent with the goal of maximizing the value of the test framework.
The template method mode solves the problems mentioned above. The intent of the template method mode is
Defines the skeleton of an algorithm operation, and delays the specific steps to be implemented in the subclass. The template method defines an algorithm in the subclass.
We do not need to change the structure of this algorithm for specific steps. This is exactly our requirement. We only require developers to know How to Write fixture.
(Setup and teardown) code, know how to write the test code. The execution sequence of fixtue code and test code is
The same is true, regardless of how the 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 runtest (){
}
Protected void setup (){
}
Protected void teardown (){
}
Since the setup and teardown methods must be overwritten and called by the framework, they are defined as protected. Design at this stage
2.
Figure 2 The testcase. Run () method applies the template method mode.
3.3 report results using testresult objects
If a testcase runs in the original forest, no one cares about its test results. You run the test to get a test note.
Describes what the test was done and what was not done.
If a test succeeds or fails, or if we run only one test, we only need to set one in the test.
When the test is complete, check the flag. However, the test success and failure opportunities are not balanced, and the test is usually successful,
Therefore, we only focus on the records of test failures. We only provide a general overview of successful records.
In Smalltalk best practice patterns, there is a mode called "collecting parameter,
When you need to collect results in multiple methods, you can pass the method a parameter or object and use this object to collect the execution results of these methods.
Result. We create a new object named testresult to collect test results.
Public class testresult extends object {
Protected int fruntests;
Public testresult (){
Fruntests = 0;
}
}
Here is a simple testresult version, which only counts the number of test runs. To use testresult, we must
Pass it as a parameter to the testcase. Run () method and notify testresult that the current test has started.
Public void run (testresult result ){
Result. starttest (this); // notify testresult to start the test.
Setup ();
Runtest ();
Teardown ();
}
Testresult tracks how many tests are running:
Public synchronized void starttest (test ){
Fruntests ++;
}
We define the starttest method in testresult as synchronous, that is, thread-safe, so a testresult object can
Collect test results from different threads. We want to keep the testcase interface simple, so we have created a version without Parameters
Run () method, which creates its own testresult object.
Public testresult run (){
Testresult result = createresult ();
Run (result );
Return result;
}
Protected testresult createresult (){
Return new testresult ();
}
Design 3 is used here.
Figure 3: testresult applies the collection parameter Mode
If the test runs correctly all the time, we do not need to write the test. We are interested in testing failures, especially those of me.
Unexpected faults. Of course, we can expect the fault to appear in the way we want it, for example, to calculate an incorrect conclusion.
Result, or a more peculiar failure method, such as writing an array out-of-bounds error. No matter how the test fails, we need to continue.
Perform subsequent tests.
JUnit distinguishes between failures and errors. Faults are predictable and can be detected using assertions. The error is
Unexpected, such as the array out-of-bounds exception (arrayindexoutofboundsexception ). Fault Identification: assertionfailederror
Error. In order to distinguish unpredictable errors from faults, the first catch statement is used for the fault, and the second catch is used for errors other than the fault.
Statement capture to ensure that other tests after this test can be run.
Public void run (testresult result ){
Result. starttest (this );
Setup ();
Try {
Runtest ();
} Catch (assertionfailederror e) {// 1
Result. addfailure (this, e );
} Catch (throwable e) {// 2
Result. adderror (this, e );
} Finally {
Teardown ();
}
}
Assertionfailederror is triggered by the assert method provided by testcase. JUnit provides many
Here is a simple example of the assert method:
Protected void assert (Boolean condition ){
If (! Condition)
Throw new assertionfailederror ();
}
Assertionfailederror faults are not captured by the test client (the test requestor in testcase), and
It is captured in the template method testcase. Run. Assertionfailederror inherits from error.
Public class assertionfailederror extends error {
Public assertionfailederror (){}
}
The methods for collecting errors in testresult are as follows:
Public synchronized void adderror (test, throwable t ){
Ferrors. addelement (New testfailure (test, t ));
}
Public synchronized void addfailure (test, throwable t ){
Ffailures. addelement (New testfailure (test, t ));
}
In the Framework, testfailure is an internal help class that maps unsuccessful tests and exceptions that occur during running
Prepare future reports.
Public class testfailure extends object {
Protected test ffailedtest;
Protected throwable fthrownexception;
}
The collection parameter requires that it be passed to every method. If we do this, each test method requires a testresult as the parameter
The signature structure of the test method is damaged. With exceptions, we can avoid the destruction of the signature structure, which is also an exception.
To take advantage of the side effects. The test case method, or the help method called by the test case, throws an exception and does not need to be known.
Testresult information. The test method in moneytestsuite can be used as an example. It indicates that the test method does not need to know testresult.
.
Public void testmoneyequals (){
Assert (! F12chf. Equals (null ));
Assertequals (f12chf, f12chf );
Assertequals (f12chf, new money (12, "CHF "));
Assert (! F12chf. Equals (f14chf ));
}
JUnit has many testresult implementations for different purposes. The default implementation is very simple. It counts the number of failures and errors and collects
Set results. Texttestresult represents the collected results in the form of text, while the JUnit test runner uses uitestresult to use
Displays the collected results in a graphical interface.
Testresult is an extension of the JUnit framework. Customers can define their own testresult classes, for example, define
Htmltestresult class, which reports test results in the form of HTML documents.
3.4. No stupid subclasses-testcase again
We use command mode to represent a test. Command Execution depends on a method like this: Execute (), which is called
Run (), which enables the command to be called, so that we can use the same interface to implement different commands.
We need a universal interface to run our tests. However, all test cases may be implemented using different methods in a class.
In this way, you can avoid creating a class for each test method, resulting in a sharp increase in the number of classes. A complex test case class also
Many different test methods are implemented. Each test method defines a simple test case. Each simple test case method has such
Name: testmoneyequals or testmoneyadd. the test case does not need to follow the simple command mode interface, the same
Different instances of the command class can call different test methods. Therefore, the next question is:
In your eyes, make all the test cases look the same.
Looking back, this problem is solved by the design pattern, and we think of the adapter pattern. The intention of the adapter mode is
The existing interfaces are transformed into the interfaces required by the customer. This meets our needs. The adapter has several different ways to do this. I
Class adapter is used to adapt interfaces. Specifically, a subclass is used to inherit existing
Class to construct the new method required by the customer using methods in the existing class. For example, to adapt testmoneyequals to runtest, we
Inherits the moneytest class and overwrites the runtest method. This method calls the testmoneyequals method.
Public class testmoneyequals extends moneytest {
Public testmoneyequals () {super ("testmoneyequals ");}
Protected void runtest () {testmoneyequals ();}
}
Using subclass adaptation requires that a subclass be implemented for each test case, which increases the burden on testers. A goal of the JUnit framework
The criterion is to keep it as simple as possible when adding a use case. In addition, creating a subclass for each test method also causes class expansion.
In many classes, there is only one method in these classes. This is not worthwhile, and it is difficult to get meaningful names for them.
Java provides an anonymous implicit class mechanism to solve the naming problem. We use an anonymous implicit class to achieve the adapter goal without naming:
Testcase test = new moneytest ("testmoneyequals "){
Protected void runtest () {testmoneyequals ();}
};
This is much more convenient than the usual subclass inheritance. It still performs type checks during compilation, at the cost of increasing the burden on developers.
Smalltalk best practice patterns describes another solution to this problem. Different instances have the same
The behavior of pluggable behavior is different. The idea is to use a class, which can be parameterized, that is, according to different parameters
Values are executed in different logic, so subclass inheritance is avoided.
The simplest form of Gable behavior is pluggable selector ). In
In smalltalk, pluggable selector is a variable that points to a method and is a method pointer. This idea is not limited
Smalltalk is also applicable to Java. There is no way to select Sub-Concepts in Java. However, Java's reflection API can be rooted in
The method name string is used to call the method. We can use the Reflection Feature of Java to implement pluggable selector. Usually
Using Java reflection, here we need to involve an underlying structure framework, which implements reflection.
JUnit provides two options for test customers: pluggable selector or anonymous implicit class. By default, we
Use the pluggable selector method, that is, the runtest method. In this way, the name of the test case must be the same as that of the test method.
To. As shown below, we use the reflection feature to call the method. First, we can view the method object. Once this method object is available, we can
To pass it parameters and call it. Because our test method does not contain parameters, we pass in an empty parameter array:
Protected void runtest () throws throwable {
Method runmethod = NULL;
Try {
Runmethod = getclass (). getmethod (fname, new class [0]);
} Catch (nosuchmethodexception e ){
Assert ("method/" "+ fname +"/"not found", false );
} Try {
Runmethod. Invoke (this, new class [0]);
}
// Catch invocationtargetexception and illegalaccessexception
}
JDK reflection API only allows us to find the public method. Therefore, you must define the test method as public. Otherwise, you will get
Nosuchmethodexception is an exception.
This is the design of this phase, adapter mode and pluggable selector mode.
Figure 4: testcase applies the adapter mode (anonymous implicit class) and pluggable selector mode.
[Add a begin translator 〕
Since there is only one runtest method in testcase, does it mean that only one test method can be put in a testcase? Introduce
Pluggable selector mode. Place multiple methods named testxxx () in testcase. When a new testcase is created, selector is used to specify
Determine which testxxx method to connect to the template method runtest.
[Add end translator 〕
3.5 don't worry about a test case or many test cases-testsuite
A system usually needs to run many tests. Now, JUnit can run a test and report the result with testresult. The next step is
Expand JUnit so that it can run many different tests. If the test caller does not care whether it is running a test or many tests, that is
It runs a test and runs many tests in the same way, so this problem is solved. The composite mode can solve this problem.
The intention of composite is to make many objects into a tree with a partial/integral structure. Composite allows customers to use the same interface
A single object and a composite object. The partial/overall hierarchy makes sense here. A combination test may have many small combinations.
A small combination test may be made up of a single simple test.
The composite mode has the following participants:
· Component: a common and unified interface for interaction with tests, whether simple or combined.
· Composite: The interface used to maintain the test set. This test set is a combined test.
· Leaf: A simple test case that complies with the component interface.
This mode requires that we introduce an abstract class which defines a unified interface for simple objects and composite objects. Its main function is
Define this interface. in Java, we directly use the interface, and there is no need to use abstract classes to define the interface, Because Java has the interface concept,
Similar to C ++, the interface does not have the concept of an interface. Using Interfaces avoids the delivery of JUnit functions to a specific base class. All tests must follow this
Interface, so what the test customer sees is this interface:
Public interface test {
Public abstract void run (testresult result );
}
The simple testcase represented by leaf implements this interface, which we have discussed earlier.
Next, we will discuss composite, a combined test case called test suite ). Testsuite uses vector for storage
Put his child (child test ):
Public class testsuite implements test {
Private vector ftests = new vector ();
}
The run () method of the test suite is delegated to its child, that is, the run () method of its child is called in sequence:
Public void run (testresult result ){
For (enumeration E = ftests. Elements (); E. hasmoreelements ();){
Test test = (TEST) E. nextelement ();
Test. Run (result );
}
}
Figure 5: The test suite uses the composite mode
To add a test to the test suite, the test customer calls the addtest method:
Public void addtest (test ){
Ftests. addelement (test );
}
Note: The code above depends on the test interface. Since both testcase and testsuite follow the same test interface
The test suite can recursively include test cases and Test suites. Developers can create their own testsuite and use it to run its
All tests.
This is an example of creating testsuite:
Public Static Test Suite (){
Testsuite suite = new testsuite ();
Suite. addtest (New moneytest ("testmoneyequals "));
Suite. addtest (New moneytest ("testsimpleadd "));
}
[Begin is helpful for understanding. Here is what the translator adds 〕
In the above Code, suite. addtest (New moneytest ("testmoneyequals") indicates to add a test to the test suite
Test: Specify the test class as moneytest and the test method as testmoneyequals. (This method is selected by selector and the template method.
Runtest connection ).
The moneytest class does not declare the moneytest (string) constructor, so the moneytest ("testmoneyequals") is called during execution.
The super (string) constructor is defined in the testcase parent class of moneytest.
Testcase (moneytest) stores the "testmoneyequals" string in a private variable. This variable is
The method pointer uses the pluggable selector mode, indicating that the method testmoneyequals specified by it must be runtest with the template method.
Connection. Testmoneyequals (),
Use the Reflection Feature of Java to call this method.
Therefore, the above Code adds two test instances to the suite, whose types are moneytest, but the test methods are different.
[End is helpful for understanding. Here we add it to the translator 〕
This example works very well, but it is stupid to manually add all tests. When you write a test case, you
Remember to add them to a static method Suite (), otherwise it will not run. Therefore, we have added a structure for testsuite.
Generator, which uses the test case class as its parameter. Its function is to extract all test methods in the class and create a test set.
And put the extracted test methods into the created test suite. However, these testing methods must comply with a simple agreement, namely
The name is prefixed with "test" without parameters. This constructor uses this Protocol to identify the tester using the Reflection Feature of Java.
And build the test object. If you use this constructor, the above code is very simple:
Public Static Test Suite (){
Return new testsuite (moneytest. Class );
}
That is, each testxxx method in the moneytest class creates a test instance. [Add a translator here 〕
However, the previous method is still useful. For example, you only want to run a subset of test cases.
3.6 Summary
The JUnit design has come to an end. Shows the mode used in JUnit design.
Figure 6: JUnit Mode
Note that testcase (core functions in the JUnit framework) participates in four modes. This indicates that in this framework, the testcase class is a "Module
Pattern Density is the center of the framework and is strongly associated with other supported roles.
The following is another view of JUnit mode. In this plot, you can see the effects of each mode in turn.
The command mode creates the testcase class, the template method mode creates the run method, and so on. All the symbols used here are from
Figure 6 removes the text.
Figure 7: Pattern plot board in JUnit
Note that when we apply the composite mode, the complexity suddenly increases. The composite mode has powerful functions.
Heart.
4. Conclusion
To draw a conclusion, we will make some general observations:
· Mode
Previously, when we were developing frameworks and trying to explain them to others, we found it useless to use patterns to discuss design. Now, you are in
It is an excellent situation to determine whether the framework is effective by using a pattern. If you like the above discussion, you will also be expressed in this way.
.
· Mode Density
There is a high mode density around testcase. testcase is a key abstraction in JUnit design. It is easy to use but hard to change. Me
We found that there is a high degree of mode density around key abstractions, which is a common phenomenon of mature frameworks. For immature frameworks, the opposite is true.
Low Mode density. Once you find out what problems you want to solve, you can "Concentrate" your solution to reach a high mode density.
· Eat your own dog food
As soon as we had the base unit testing functionality implemented, we applied it ourselves.
A testtest verifies that the framework reports the correct results for errors, successes,
And failures. We found this invaluable as we continued to evolve the design of
Framework. We found that the most challenging application of JUnit was testing its own
Behavior.
· Intersection rather than merge
In framework development, you always want to include every feature and make the framework as valuable as possible, but there is another factor opposite: You want to develop
Use your framework. The fewer features the framework has, the easier it is to learn, and the more likely developers will use it. This is how JUnit is designed.
It is essential for running the test, such as running the test suite, separating different tests from each other
Dynamic run test and so on. Of course we will add new features, but we will carefully select them and put them into the JUnit extension package.
In the extension package, a noteworthy member is the testdecorator class, which uses the decorator mode and can be run in the test code.
Execute other code before or after running. [Added here 〕
· Framework authors spend a lot of time reading framework code
We spend a lot of time reading the Framework Code than writing the code. We add functionality to the framework, but we spend the same amount of time deleting it.
In addition to the repeated functions in the framework. We use various ways to design the framework, add class and mobile class responsibilities, as long as we can consider various ways
Path. In the work of JUnit, testing, Object design, framework development, and writing articles, we constantly improve our insights and benefit from them.