Unit tests are the most fundamental part of the entire test process, and they require programmers to identify problems as early as possible and to give control, which is one. Also, if there is a problem with the integration test, they can help diagnose. This lays a solid foundation for establishing an efficient event response mechanism in the software development process.
JUnit provides a framework for Java program developers to implement unit tests, making Java unit tests more prescriptive and effective, and more conducive to testing integration.
JUnit's internal structure
JUnit's software architecture
JUnit has a total of seven packages, the core of which is junit.framework and Junit.runner. The framework package is responsible for the architecture of the entire test object and runner is responsible for testing the driver.
JUnit's class structure
JUnit has four important classes: TestSuite, TestCase, TestResult, Testrunner. The first three classes belong to the framework package, and the latter class is different under different circumstances. The text test environment is used here, so the Junit.textui.TestRunner is used. The responsibilities of each class are as follows:
1.TestResult, responsible for collecting the results performed by TestCase, divides the results into two categories, customer-predictable failure and no predictive error. It is also responsible for forwarding the test results to Testlistener (the interface is inherited by Testrunner) processing;
2.TestRunner, the starting point of the customer object invocation, is responsible for tracking the entire test process. Be able to display the returned test results and report the progress of the test.
3.TestSuite, responsible for packaging and running all the testcase.
4.TestCase, the class to which the client tests the class, initializes the client class when testing, and tests the method call.
There are two other important interfaces: Test and Testlistener.
1.Test, contains two methods: Run () and counttestcases (), which is the extraction of the test action feature.
2.TestListener, contains four methods: Adderror (), Addfailure (), Starttest (), and Endtest (), which are the processing of test results and the extraction of the action characteristics of the test-driven process.
The two class diagrams given below (with limited space, showing only the main parts) illustrate the relationship between classes and the design goals of JUnit (1). The class of the test case takes the composite pattern. In this way, the customer's test object is transformed into a "partial-whole" hierarchy. Customer code only needs to inherit the class TestCase, it can easily be used in combination with other existing objects, which makes unit test integration more convenient.
Figure 1 Test structure diagram
Figure 2 is a test tracking class diagram. Figure 2 The left Testsuite contains a collection of test objects, and the right side contains the test result set. How to deal with the results, and what test objects are included, are not immediately conclusive, but rather deferred to a specific implementation. For example, junit that implements interface Testlistener contains: Junit.awtui.TestRunner, Junit.swingui. Testrunner, Junit.ui.TestRunner and so on, even customers with their own classes to achieve testlistener, so as to achieve the purpose of diversification.
Figure 2 Test the trace diagram
From the above two class diagrams, you can learn about JUnit's basic approach to unit testing, the core of which is the result set and case set.
The implementation process of JUnit
The typical way to use JUnit is to inherit the TestCase class and then overload it with some important methods: SetUp (), teardown (), Runtest () (These are optional), and finally assemble these customer objects into a Testsuite object, which is handed to the Junit.textui.TestRunner.run (case set) driver. The following analysis of how the case set works.
Figure 3 basically illustrates JUnit's Test flow architecture. We will analyze this diagram in detail from different angles.
Figure 3 Test sequence diagram
First, the object is created from the analysis. The customer class is responsible for creating the suite and Atestrunner. Note that the class Testrunner contains a static function run (Test), which is created from itself and then called Dorun (). The general invocation of the client class is the function, whose code is as follows:
static public void Run (Test suite)
{
Testrunner atestrunner= new Testrunner ();//New test driver
Atestrunner.dorun (suite, false);//Run test set with test drive
}
The Suite object is responsible for creating numerous test cases and accommodating them to itself. The customer test case inherits the TestCase class, which passes the class, not the object, to the Suite object. The Suite object is responsible for parsing these classes, extracting the constructors, and the methods to be tested. The test case is constructed using the method to be tested, and the fname of the test case is the name of the method to be tested. The test result set is created by Atestrunner. This seems somewhat contradictory to the class diagram that was previously elaborated, where a test set can contain many different test drivers, and it seems to be desirable to create a result set first. Obviously, there is only one way to deal with the test results here, so it's also possible to do so.
Second, from the execution of the test action, the test is really starting with suite.run (result). The code is as follows:
public void Run (TestResult result)
{
Get all the test cases from the case set and execute them separately
For (enumeration e= tests (); e.hasmoreelements ();)
{
if (Result.shouldstop ())
Break
Test test= (Test) e.nextelement ();
Runtest (test, result);
}
}
Once the test case is executed, the first callback policy is used to get itself to result. With every step of this test, the test driver Atest runner can track the processing. This virtually creates a large surveillance system that can be used at any time to give different levels of attention to the events that have occurred.
We analyze the design pattern of the action behavior involved:
1. Template method class behavior pattern, the essence of which is to first establish the skeleton of the method, and as far as possible to the concrete implementation of the method backwards. Testcase.runbare () uses this pattern, and the client class can reload its three methods, which improves the scalability of the test.
public void Runbare () throws Throwable
{
SetUp ();
try {runtest ();}
finally {TearDown ();}
}
2. Command-object behavior mode, which essentially encapsulates an action as an object without caring for the recipient of the action. The receiver of this action can be determined only when the action is executed. Interface test is a command set so that different test methods of different classes can construct their frame structure through the same interface test. This brings a lot of convenience to the integration of the test.
The throwing mechanism of JUnit's exception
JUnit's exception level is divided into three layers: 1.Failure, customer-predicted test failure, can be detected by the Assert method; 2. Error, the accident caused by the customer test; 3. Systemerror, JUnit has a thread death-level exception, which is rarely the case. These three anomalies of JUnit are well represented in the runprotected () method of the TestResult class. Here the Protectable interface is used to encapsulate the test execution method, in fact, P.protect executes is test.runbare ().
public void runprotected (final Test test, protectable p)
{
try {p.protect ();}
catch (Assertionfailederror e)
{addfailure (test, e);}
catch (Threaddeath e)
{rethrow e;}
catch (Throwable e)
{adderror (test, e);}
}
The code first checks whether it is assertion failederror and then determines if it is a serious threaddeath. This exception must be rethrow in order to guarantee the thread's true death, if not, to indicate that it is an accident.
The first two exceptions are saved in the test result set until the entire test is complete and printed out for customer reference.
Some suggestions for implementing JUnit
From the above analysis, you can understand the structure and process of junit, but in the actual application of JUnit, there are a few suggestions also need to explain, as follows:
1. The customer class can overload runtest (), and its default implementation is to call a test method named FName. If the customer does not use Testsuite to load the testcase, it is particularly necessary to overload it, which is of course not in favor of use, not conducive to integration. In addition, the functions of setUp () and teardown () seem to be similar to constructors, but if there is a class-inheritance relationship between test cases, initializing some parameters with constructors will result in confusing data, which is not conducive to determining the validity of test results.
2. The order of calls for the function to be tested is indeterminate, and the data structure used is vector (). If you need to have a sequential relationship, you can combine them together and then use the same test method.
3. In order to make the test results clear, it is best not to have the printout in the program, or the printout of the program and the printout of the JUnit test should not be system.out with the same data source. In fact, this is the two test habits, the direct printing output is more traditional, from the test motivation to consider it is also more casual, and the results need to be manually observed. If there are more direct printouts, the viewer may not be able to obtain satisfactory results.
Junity Unit Test