Introduction to Java open-source testing tool JUnit

Source: Internet
Author: User

1. Introduction

In an earlier article (see test infected: programmers love writing tests, Java Report, July 1998, Volume 3, number 7, we describe how to use a simple framework to write repeated tests. In this article, we will take a quick look at the details and show you how the framework is constructed.

We carefully study the juint framework and think about how to construct it. We have discovered many lessons at different levels. In this article, we will try to communicate with them immediately, which is a desperate task, but at least it is done in the context where we show you how to design and construct a software with proven value.

We raised a discussion about the Framework objectives. During the expression of the framework itself, the target will repeat many small details. Later, we proposed the design and implementation of the Framework. The design will be described from the perspective of the pattern (surprise, surprise) and implemented as a beautiful program. We have summarized some excellent ideas about framework development.

2. What is JUnit's goal?

First, we have to go back to the assumption of development. In the absence of an automatic test program feature, we assume that it cannot work. This seems safer than the mainstream assumption. The mainstream assumption is that if developers assure us that a program feature can work, it will always work now and in the future.

From this point of view, when developers write and debug Code, their work is not completed, and they must write a test to demonstrate that the program can work. However, everyone is too busy, they have to do too much, and they don't have enough time for testing. I already have too many code to write. How can I write the test code again? Answer me, tough project manager. Therefore, the primary goal is to write a framework in which developers can see the hope of writing tests. The framework must use common tools, so there won't be much new things to learn. It cannot do more than writing a new test. Repetitive work must be excluded.

If all tests are done like this, you can write expressions in a single debugger. However, this is not adequate for testing. It doesn't help me to tell me that your program can work now, because it doesn't assure me that your program will work every minute after I integrate it, and it does not assure me that your program will still be able to work for five years, and you have been away for a long time.

Therefore, the second goal of the test is to generate a test that can maintain its value continuously. Others except the original author must be able to perform the test and explain its results. The tests of different authors should be combined and run together without worrying about conflicts.

Finally, you must be able to generate a new test based on the existing test. Generating a setup or fixture is expensive and a frame must be able to reuse the fixture to run different tests. Oh, is there anything else?

3. JUnit Design

The JUnit design will be presented in a style used in patterns generate ubuntures (see "patterns generate ubuntures", Kent Beck and Ralph Johnson, ecoop 94) for the first time. The idea is to explain the design of a system by applying the model from scratch, one by one, until you obtain the system architecture. We will propose the architecture problems to be solved, summarize the models used to solve the problems, and then demonstrate how to apply the models to JUnit.

3.1 start-testcase

First, we must construct an object to express our basic concept, testcase (test case ). Developers often have test cases in their minds, but there are many different ways to implement them-

· Print statement

· Debugger expression

· Test script

If we want to easily manipulate tests, we must build them into objects. This will get a test that is only hidden in the developer's mind and made specific. It supports the purpose of creating a test, that is, to continuously maintain their value. At the same time, object developers are more accustomed to using objects for development. Therefore, the decision to build a test into an object supports our goal-making the test writing more attractive (or at least not gorgeous ).

Command mode (see gamma, E ., et al. design Patterns: Elements of reusable object-oriented software, Addison-Wesley, reading, Ma, 1995) can better meet our needs. Extract the intent (intent), "encapsulate a request into an object so that you can parameterize the customer with different requests; queue the request or record the request log... "Command tells us that we can generate an object for an operation and give it an" execute "method. The following code defines the testcase class:

Public abstract class testcase implements test {
...
}

Because we expect that this class can be reused through inheritance, we declare it as "public abstract ". Temporarily ignore the fact that it implements the interface test. Given the needs of the current design, you can regard testcase as an isolated class.

Each testcase must have a name when it is created. Therefore, if a test fails, you can identify the test that fails.

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 will use a diagram to demonstrate the architecture snapshot ). The tag we use is simple. It uses a sharp box containing the relevant mode to mark the class. When the role of the class in the mode is obvious, only the name of the mode is displayed. If the role is not clear, add the name of the participant associated with the class in the tip box. This label minimizes the confusion of an image and is first seen in applying Design Patterns in Java (see gamma, E ., applying Design Patterns in Java, in Java gems, Sigs reference library, 1997 ). Figure 1 shows the tag applied to testcase. Because we are dealing with a separate class and there is no ambiguity, only the name of the mode is displayed.


Figure 1 testcase Application Command 3.2 blank filling-run ()

The next problem to be solved is to give developers a convenient "place" for storing their fixture code and test code. Declaring testcase as abstract means that the developer wants to reuse testcase through subclassing. However, if all we can do is provide a super class with only one variable and no behavior, so we won't be able to do much work to meet our first goal-making testing easier to write.

Fortunately, all tests have a common structure-create a test fixture, run some code on the fixture, check the results, and then clean the fixture. This means that each test will run with a new fixture, and the results of one test will not affect the results of other tests. This supports the goal of maximizing test value.

The template method involves our problems. Describe the intent, "define the skeleton of an algorithm in an operation and delay some steps to the subclass. The template method allows the subclass to redefine certain steps of an algorithm without changing the structure of an algorithm ." This is perfectly appropriate. We want developers to consider how to write the jigs (build and disassemble) Code and how to write the test code separately. In either case, the execution order remains the same for all tests, regardless of how the jigs code is written or how the test code is written.

The template method is as follows:

Public void run (){
Setup ();
Runtest ();
Teardown ();
}

These methods are implemented by default as "nothing is done ":

Protected void runtest (){}
Protected void setup (){}

Protected void teardown (){}

Since setup and teardown will be used to override (override) and will be called by the framework, we declare them as protected. Our second snapshot 2 is shown.
Figure 2 testcase. Run () apply Template Method

3.3 Results report-testresult

If a testcase runs in the forest, does anyone care about the result? Of course-the reason you run the test is to confirm that they can run. After the test is completed, you want a record, a summary of what can be done and what cannot be done.

If the test has an equal chance of success or failure, or if we have just run a test, we may just set a flag in the testcase object and check the flag when the test is complete. However, testing (often) is very uneven-They usually work. Therefore, we only want to record failures and a highly concentrated Summary of success.

The Smalltalk best practice patterns (see Beck, K. Smalltalk best practice patterns, Prentice Hall, 1996) has a applicable mode called collecting parameter (Collection parameter ). It is recommended that when you need to collect results among multiple methods, you should add a parameter in the method and pass an object to collect results for you. We create a new object, testresult (test result), to collect the running test results.

Public class testresult extends object {
Protected int fruntests;

Public testresult (){
Fruntests = 0;
}
}

In this simple version, testresult can only calculate the number of tests that run. To use it, we have to add a parameter to the testcase. Run () method and notify testresult that the test is running:

Public void run (testresult result ){
Result. starttest (this );
Setup ();
Runtest ();
Teardown ();
}

In addition, testresult must remember the number of tests run:

Public synchronized void starttest (test ){
Fruntests ++;
}

We declare the strattest method of testresult as synchronized, so that when the test runs in different threads, a separate testresult can safely collect the results. Finally, we want to maintain the simple external interface of testcase. Therefore, we create a run () version without any parameters, which is responsible for creating our own testresult.

Public testresult run (){
Testresult result = createresult ();
Run (result );
Return result;
}
Protected testresult createresult (){
Return new testresult ();
}

The following design snapshot is shown in 3.
Figure 3 applying collecting parameter to testresult

If the tests always run correctly, we do not need to write them. Testing is interesting only when the test fails, especially when we don't expect them to fail. What's more, testing can fail as we expected, for example by calculating an incorrect result, or they can fail in a more attractive way, for example by writing an array out of bounds. No matter how the test fails, we want to perform the subsequent test.

JUnit distinguishes failures from errors ). The possibility of failure is predictable and can be checked using assertion. Errors are unpredictable, such as arrayindexoutofboundsexception. An assertionfailederror can be used to send an error. To identify an unexpected error and a failure, the failure is captured in the catch clause (1. Clause (2) captures all other exceptions and ensures that our test can continue to 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 ();
}
}

The assert method provided by testcase triggers an assertionfailederror. JUnit provides a set of assert methods for different purposes. The following is the simplest one:

Protected void assert (Boolean condition ){
If (! Condition)
Throw new assertionfailederror ();
} ([Translator's note] The assert in the latest JUnit release version has been changed to asserttrue due to conflicts with the assert keyword in JDK .)

Assertionfailederror should not be captured by the customer (a test method in testcase), but by testcase. Run () in the template method. Therefore, assertionfailederror is derived 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 ));
}

Testfailure is a small helper class inside the framework, which binds failed tests and exceptions that send signals to subsequent reports.

Public class testfailure extends object {
Protected test ffailedtest;
Protected throwable fthrownexception;
}

The standard form of collecting parameter mode requires us to pass collecting parameter to every method. If we follow this suggestion, the testresult parameter is required for each test method. It will cause "pollution" of the signature of these methods ". Sending failure with exceptions can be a friendly side effect, so that we can avoid such signature contamination. A test case method, or a helper method, can throw an exception without having to know testresult. As a learning material, a simple test method is provided here. It comes from our moneytest suite. For more information, see JUnit test infected: programmers love writing tests ). It demonstrates how a test method does not have to know any information about testresult.

Public void testmoneyequals (){
Assert (! F12chf. Equals (null ));
Assertequals (f12chf, f12chf );
Assertequals (f12chf, new money (12, "CHF "));
Assert (! F12chf. Equals (f14chf ));
} ([Translator's note] The assert in the latest JUnit release version has been changed to asserttrue due to conflicts with the assert keyword in JDK .)

JUnit proposes different implementations of testresult. By default, the number of failures and errors is counted and the results are collected. Texttestresult collects results and expresses them in the form of text. Finally, the graphic version of JUnit test runner uses uitestresult to update the graphical test status.

Testresult is an extension point of the framework ). Customers can customize their testresult classes. For example, htmltestresult can report the results as an HTML document. 3.4 non-stupid subclass-let's talk about testcase

We have applied command to present a test. Command depends on a separate method like execute () (called run () in testcase) to call it. This simple interface allows us to call different implementations of a command through the same interface.

We need an interface to run our tests in a general way. However, all test cases are implemented as different methods of the same class. This avoids unnecessary class diffusion (proliferation of classes ). A given test case class can implement many different methods. Each method defines a separate test case ). Each test case has a descriptive name, such as testmoneyequals or testmoneyadd. The test case does not conform to the simple command interface. Different instances of the same command class must be called with different methods. Therefore, the following problem is that all test cases are the same from the perspective of test callers.

Looking back at the problems involved in the current available design patterns, the adapter pattern comes to mind. The adapter has the following intent: "converting an interface of a class to another interface that the customer wants ". This sounds perfect. The adapter tells us different ways to do this. One of them is the class adapter, which uses subclass to adapt to the interface. For example, to adapt testmoneyequals to runtest, we implement a moneytest subclass and override the runtest method to call testmoneyequals.

Public class testmoneyequals extends moneytest {
Public testmoneyequals () {super ("testmoneyequals ");}
Protected void runtest () {testmoneyequals ();}
}

To use subclass, we need to implement a subclass for each test case. This puts an extra burden on the tester. This is contrary to JUnit's goal, that is, the framework should be as simple as possible to increase the number of test cases. In addition, creating a subclass for each test method causes class bloat ). Many classes will have only one separate method. This overhead is not worthwhile and it is difficult to propose meaningful names.

Java provides an anonymous internal class (anonymous inner class), which provides a special solution for interesting Java to solve class naming issues. Using an anonymous internal class, we can create an adapter without having to create a class name:

Testcase test = new moneytest ("testmoneyequals "){
Protected void runtest () {testmoneyequals ();}
};

This is much easier than fully subclass. The compile-time type checking is maintained at the cost of the developer's workload during the compilation period ). Smalltalk Best Practice Pattern describes another solution to solve the problem of different instances. These instances are different under the title of the common pluggable behavior. This idea is to use a separate parameterized class to execute different logic without subclass.

The simplest form of pluggable behavior is pluggable selector ). Pluggable selector saves a Smalltalk selector method in an instance variable. This idea is not limited to Smalltalk, but also applicable to Java. There is no selector method tag in Java. However, the Java reflection (reflection) API allows us to call this method based on the representation string of a method name. We can use this feature to implement a Java version of pluggable selector. When talking about this topic, we usually do not use reflection in common applications. In our case, we are dealing with an infrastructure framework, so it can wear a reflection.

JUnit allows customers to choose either pluggable selector or implement the anonymous adapter class mentioned above. For this reason, we provide pluggable selector as the default implementation of the runtest method. In this case, the name of the test case must be consistent with that of a test method. As shown below, reflection is used to call the method. First, we will look for the method object. Once a method object is available, it is called and its parameters are passed. Because our test method has no parameters, we can pass 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
}

The reflection API of jdk1.1 only allows us to find public methods. For this reason, you must declare the test method as public. Otherwise, a nosuchmethodexception exception is returned.

The following design snapshot is added to the adapter and pluggable selector.
Figure 4 testcase application adapter (with an anonymous internal class) or pluggable Selector

3.5 ignore one or more-testsuit

To gain confidence in the system status, we need to run many tests. So far, JUnit has been able to run a separate test case and report the results in a testresult. The next challenge is to expand it so that it can run many different tests. When the test caller does not have to worry about running one or more test cases, this problem can be easily solved. A popular model that can overcome difficulties in this situation is composite ). Extract the intent, "combine the object into a tree structure to represent the 'partial-all' hierarchy. Composite makes the use of a single object and a composite object consistent ." Here, the 'part-holistic 'hierarchy is interesting. We want to support Test suites that can be layered.

Composite introduces the following participants:

· Component: Declares the interface we want to use to interact with our tests.

· Composite: implements this interface and maintains a set of tests.

· Leaf: represents a test case in composite, which complies with the component interface.

This mode tells us to introduce an abstract class to define public interfaces for individual objects and composite objects. The basic intention of this class is to define an interface. When using composite in Java, we prefer to define an interface instead of an abstract class. The interface avoids submitting JUnit into a specific base class for testing. It is necessary that these tests comply with this interface. Therefore, we modify the description of the mode and introduce a test interface:

Public interface test {
Public abstract void run (testresult result );
}

Testcase corresponds to a leaf in composite and implements the interface we saw above.

Next, we will introduce the participant composite. We named it testsuit (test suite) class. Testsuit saves its child test in a vector ):

Public class testsuite implements test {
Private vector ftests = new vector ();
}

The run () method delegates its submembers (delegate ):

Public void run (testresult result ){
For (enumeration E = ftests. Elements (); E. hasmoreelements ();){
Test test = (TEST) E. nextelement ();
Test. Run (result );
}
}

Figure 5 test suit application Composite

Finally, the customer must be able to add the test to a suite and they will use the addtest method to do so:

Public void addtest (test ){
Ftests. addelement (test );
}

Note how the above Code only depends on the test interface. Because both testcase and testsuit comply with the test interface, we can recursively combine the test suite into a new suite. All developers can create their own testsuit. We can create a testsuit that combines these suites to run all of them.

The following is an example of creating testsuit:

Public Static Test Suite (){
Testsuite suite = new testsuite ();
Suite. addtest (New moneytest ("testmoneyequals "));
Suite. addtest (New moneytest ("testsimpleadd "));
}

This works fine, but it requires us to manually add all tests to a suite. Early JUnit writers told us that this is stupid. As long as you write a new test case, you must remember to add it to a static suit () method, otherwise it will not run. We added a convenient construction method of testsuit, which uses the test case class as a parameter. The intent is to extract (extract) the test method and create a suite containing the test methods. A simple Convention that must be followed for the test method is that it is prefixed with "test" without parameters. The constructor uses this Convention to construct a test object by using the reflection discovery test method. With this constructor, the above Code will be simplified:

Public Static Test Suite (){
Return new testsuite (moneytest. Class );
}

When you just want to run a subset of the test case, the initial method will still be useful. 3.6 conclusion

Now we are at the end of JUnit. Describe JUnit's design from the perspective of mode, as shown in.
Figure 6 Summary of JUnit Mode

Note that as the center of the Framework abstraction, testcase is related to the four modes. The mature object design description shows the same "pattern density ". The center of the design is a rich set of relationships that are associated with the supported participants (players.

This is another way to look at all the modes in JUnit. The influence of each pattern is abstracted on the storyboard in turn. Therefore, the command mode creates the testcase class, the template method mode creates the run method, and so on. (The plot board tag deletes all text based on the tag in Figure 6 ).
Figure 7 plot board of JUnit Mode

One thing to note about the plot board is how the complexity of the diagram jumps when we apply composite. It proves our intuition in graphical form, that is, composite is a powerful pattern, but it will "complicate the graph ." Therefore, use it with caution.

4 Conclusion

Finally, let's make some comprehensive observations:

· Mode

We found that it is very valuable to discuss design from the perspective of patterns, whether in our framework development or when we try to discuss it to others. You are now in a perfect position to determine whether a framework is effective in a pattern. If you like the above discussion, try the same performance style for your own system.

· Mode Density

The mode "density" around testcase is relatively high, which is a key abstraction of JUnit. The design with high mode density is easier to use, but more difficult to modify. We found that such a high Pattern Density around key abstractions is common for mature frameworks. The opposite should apply to immature frameworks-they should have low mode density. Once you find the problem you want to solve, you can start the "compress" solution until a region with more and more intensive modes, these models provide leverage in them.

· Use your own things

Once we have completed the basic unit test function, we need to apply it ourselves. Testcase can verify that the framework reports correct results for errors, successes, and failures. We found that as the Framework Design continues to evolve, this is priceless. We found that the most challenging application of JUnit is to test its own behavior.

· Intersection rather than Union)

There is a temptation in framework development to include every feature you can have. After all, you want to make the framework as valuable as possible. However, there will be a hindrance-developers have to decide to use your framework. The less features a framework has, the easier it will be to learn and the more likely developers will use it. JUnit is based on this style. It only implements the basic features of the test run-test suite, so that the execution of each test is isolated from each other and the test runs automatically. Yes, we cannot resist adding some features, but we will be careful to put them in their own extension packages (test. Extensions ). The package contains testdecorator, which allows you to execute additional code before and after a test.

· Framework writers should read their code

We spend much more time reading JUnit code than writing it. In addition, the time spent on removing repeated features is almost the same as the time used to add new features. We actively conduct design experiments to add new classes and move roles in different ways that we can come up. Through continuous insight into JUnit (testing, Object design, framework development) and the opportunity to publish more in-depth articles, we are rewarded for our paranoia (and will still be rewarded ).

The latest JUnit version can be downloaded from ftp://www.armaties.com/d/home/armaties/ftp/testingframework/junit.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.