JWalk software testing tool suite

Lazy systematic unit testing for agile methods

You are here: JWalk Home / User Guide /
Department of Computer Science

The JWalk User Guide

JWalkTester exercising the Full state

The JWalk 1.1 Tool Suite comes with two finished testing tools, JWalkTester, the flagship GUI-based unit testing tool, and JWalkUtility, the classic command-line driven unit testing utility. Also, the unbundled software contains the JWalker tool kit for building custom JWalk applications. Familiarise yourself with the common JWalking concepts below, then refer to the specific guides for each tool or toolkit (see margin).

JWalking Concepts

What is JWalking? Well, it is a fast and effective way to do Java unit testing. All the tools need is a compiled Java class to test, and some indication of what test strategy to follow. You may inspect the test class's interface, explore its algebraic structure or state-space, exercise sequences of methods to examine its visible behaviour, or train a test oracle to recognise correct or incorrect test outcomes. Once a test oracle has been trained, testing is fully automatic. Also, if the test class is modified and recompiled, the oracle can be re-trained in a short time. Oracles can also learn from superclass oracles, but adapt quickly to the subclass, learning about combinations of local and inherited methods. The important thing is that the tools determine the right method sequences that need to be tested, and explore the test class completely, to bounded depths.

Test Class

One unit, a compiled Java class, may be tested at a time. This is known as the test class, which must always be supplied by the tester and uploaded by the testing tool. Other test parameters have suitable default settings, which the tester may choose to override. The test class could be a single small component, or a large subsystem delegating to other classes, in which case tests may also invoke methods on the connected classes. If you want to replace production classes with stub- or mock-objects, then refer to the section on how to create Custom Generators for JWalk.

The test class may exist in the default package, or in a package under the current test class directory (by default, the current working directory), or in some other package known to the Java CLASSPATH. The test class must always be loaded using its package-qualified name: if you get the path wrong, the tools will report failure to find or to load the test class. The test class directory may be changed in some tools.

Test Strategy

The exploration of the test class can follow different test strategies, which range from the brute force breadth-first exploration of all method combinations, to focusing on primitive algebraic constructions that reach all low-level concrete states, to the sophisticated analysis of high-level states and transitions.

  • protocol strategy: creates test sequences corresponding to every possible interleaved ordering of the test class's public constructors and methods (all method protocols);
  • algebra strategy: creates test sequences corresponding to every novel construction, that is, sequences which drive the test object into different concrete states (all algebraic constructions);
  • states strategy: creates test sequences corresponding to the state cover, transition-cover and n-switch transition cover, which drive the test object through its high-level state machine (all states and transitions).
The algebraic structure is detected using low-level object state comparison, which identifies when methods change state, leave state unchanged, or cause an object to revisit an earlier state. The high-level abstract states are detected using natural state predicate methods supplied by the test class. For example, if a Stack class provides predicates isEmpty() and isFull(), three states will be discovered: {Empty, Default, Full}. Some test classes may only have a single Default state, whereas others may have only named high-level states.

Test Modality

The different test modalities control whether you wish merely to inspect or analyse the test class, or also explore its behaviour by executing different test sequences, or also validate its behaviour by training a test oracle, which is later saved in an oracle data file:

  • inspect modality: performs a static analysis (and optionally a dynamic analysis) of the test class, to reveal the public API, or algebraic structure, or state space;
  • explore modality: also exercises the test class by generating and executing test sequences according to the current test strategy, displaying results for the tester to view;
  • validate modality: also validates the outcomes of each test sequence, comparing the actual result against the result predicted by a test oracle, which is trained interactively and saved when testing is over.
A test oracle is saved in, and reloaded from, an oracle data file, which is placed in the oracle directory (by default, the test class directory). The file is a plain text file, whose name consists of the short name of the test class, followed by the extension: .jwk. For example, an oracle for the class org.jwalk.test.Stack will be saved in the file Stack.jwk in the current oracle directory, which can be changed in some tools.

Depth Settings

The testing algorithms used by JWalk perform bounded exhaustive exploration of the test class's state space. All exploration and validation is carried out to a bounded depth. Furthermore, the initial dynamic analysis, and the comparison of object states, are also affected by custom depth settings.

  • testDepth: controls the maximum length of test sequences generated during exploration (and validation) - test sequences of every length up to this limit are exercised, counting initial object construction (or a state-cover sequence) as depth zero;
  • probeDepth: controls the maximum length of probing sequences generated during the initial dynamic analysis (in certain strategies) - set this higher to discover missing states and lower to avoid memory exhaustion;
  • stateDepth: controls how deeply objects are examined when their states are compared for equality - set this to zero for shallow equality and higher for deeper equality (of object trees), especially if a mutable object structure appears not to change state.

Value Generators

The JWalk strategy algorithms compute what method sequences to execute. Before execution, each constructor or method must be supplied with suitable test inputs. These are supplied by objects known as Generators, which are capable of synthesising values of given Java types. Generators must synthesise values in an exactly repeatable, deterministic order (rather than randomly). This allows the JWalk tools to learn the correct test outcomes, when the same test inputs are presented in subsequent test cycles. There are two kinds of Generator, described by the Java interfaces:

  • MasterGenerator: produces a standard series of quasi-unique test values (and objects) in a monotonically increasing sequence;
  • CustomGenerator: produces a custom series of test values (and objects) under the explicit control of the tester.
The tester may supply their own CustomGenerators, to control the context of testing and the order in which test values are presented to the test class. For more details, refer to the section on how to create Custom Generators for JWalk. CustomGenerators are compiled Java classes, loaded from the current generator directory (by default, the test class directory), which may be changed in some tools.

Convention on Inheritance

The JWalk strategy algorithms exercise all public methods of the test class, including those inherited from ancestor classes, all the way up to the root Object class. However, interleaving all of Object's built-in methods is sometimes a nuisance, so these methods may be selectively excluded by convention. There are three conventional settings:

  • standard - exclude all of Object's methods
  • custom - include some of Object's methods
  • complete - include all of Object's methods
In the custom setting, the following four of Object's methods are included: equals(), hashCode(), toString() and getClass(). If the test class is in fact the root class java.lang.Object, this overrides any convention, and all of its methods are included.

Standard Files and Directories

The JWalk tools need to read and write certain data files during their normal operation. These files are few in number, known to the user, and exist in standard locations under the installation directory. The files that JWalk routinely reads and/or writes are the following:

  • the JWalk license file, JWalkLicense.txt, which must live in the working directory from which the tools are launched;
  • the oracle data file, which is created for each test class when the tools are run in validation mode, and exists either in the test class directory, or in a separate oracle directory specified by the tester;
  • the redirected input file, input.txt, which is created for each test class directory, if input is redirected by uploading the custom RedirectInGenerator.
Of these, the oracle data file and the redirected input file may be safely deleted, when no longer required. The oracle data file may be edited by hand (so long as the format is respected) to cause JWalk to learn or forget parts of a specification. The redirected input file may be edited to reflect the console input required by the test class. The JWalk license file must not be changed in any way and may not be transferred, except to a new working directory on the same machine.

Indiscriminate File Creation

Warning: If the JWalk tools are used in an unguarded way, it is possible to excercise user-defined classes that create or destroy files and directories indiscriminately. If user-defined classes have methods that accept File or Stream parameters, then the JWalk tools will attempt to create instances of these. In principle, exercising an object that creates or deletes files and directories could cause arbitrary havoc to the filesystem, but in practice, this is mitigated in two ways:

  • all created directories and files should appear below the current working directory, and have automatically generated names corresponding to the conventional strings emitted by the JWalk tools;
  • the tester may take control of how and where files and directories are created, by supplying a CustomGenerator that intercepts the creation of File and Stream types.
We have tested the worst consequences of this by exercising Java's File class exhaustively, to depth 15. This causes the creation of a directory named aubergine under the current working directory, which is then populated by hundreds of temporary files, with strange synthesised names. Apart from the time taken to create so many files, no long-term damage was suffered and later this directory was removed.

JWalkTester analysing a ReservableBook

JWalking Method

The following describes the general approach followed when exploring and testing a prototype class using the JWalk 1.1 Tool Suite. This method applies both to the finished testing tools JWalkTester and JWalkUtility; and to any application built using the JWalker tool kit.

Inspecting the Test Class

After loading the test class, you may choose to inspect it. Depending on the test strategy chosen, you will obtain a different analysis:

  • protocol inspection: performs a static analysis of the test class, showing its public constructor and method interface (if the test class is an enumerated type, its constants are displayed);
  • algebra inspection: also performs a dynamic analysis of the test class's algebraic structure, classifying its operations into primitive, transformer and observer operations;
  • state inspection: instead, performs a dynamic analysis of the test class's high-level state space, using the natural state predicates supplied by the class to help identify states.

To illustrate algebraic analysis, a ReservableBook class provides a number of methods, which are classified. The initial constructor and methods issue() and reserve() are classified as primitive, because they drive the book into all of its different concrete states. The methods discharge() and cancel() are classified as transformers, because they return the book to an earlier state. The methods getBorrower() and isOnLoan() are classified as observers, because they do not modify the book's state.

To illustrate high-level state analysis, if a ReservableBook class provides the two state predicate methods isOnLoan() and isReserved(), then the state detection algorithms will find four high-level states: {Default, OnLoan, Reserved, Reserved&OnLoan}, by seeking to reach concrete states in which each predicate returns true or false. The Default state is the one in which no predicate returns true. The other states are named after the predicates which return true. The shortest test sequences reaching these distinct high-level states are collected in the state cover test set, which forms the starting basis for state-based testing (see below).

Exploring the Test Class

Next, you may wish to explore the behaviour of the test class in some detail. You might do this to check that you haven't overlooked interesting combinations of methods that might yield unusual results. You will start with a low testDepth setting, and increase this as you grow confident that the shorter sequences behave correctly. The results you get depend on the test strategy chosen:

  • protocol exploration: executes all constructors followed by all interleaved combinations of methods (including inherited methods), producing some unusual sequences in which the same methods are repeatedly invoked, or are invoked in unexpected orders;
  • algebra exploration: executes all constructors followed by only those methods (including inherited methods) that drive the test object into new concrete states, producing sequences that grow the test object and observe its behaviour at the edges;
  • state exploration: executes the state cover (see above) extended by all interleaved combinations of methods (including inherited methods), producing a set of test results for each high-level state, explored to each depth.

Algebraic exploration is perhaps the most compact strategy to use at first, when investigating all the expected behaviours of the test class. For example, the prototype ReservableBook is explored in this mode, revealing an interesting combination at testDepth 3:

   ReservableBook target = new ReservableBook();
         target.reserve(Borrower Borrower#1);
         target.issue(Borrower Borrower#2);
         target.getBorrower();
      ==> Borrower#2 : NORMAL  (unchanged)
which fails to take into account the business rules of reservation. The programmer may quickly fix the code so that books may only be issued to the borrower that reserved them.

Protocol exploration is good for identifying boundary cases like null operations and for checking that observer-methods do not unexpectedly modify the object's state. For example, when the ReservableBook is explored, this yields some interesting sequences that include:

   ReservableBook target = new ReservableBook();
         target.isOnLoan();
         target.discharge();
         target.discharge();
      ==> void : NORMAL  (unchanged)
This makes the programmer think about what should happen if a book is discharged, before being issued on loan to any borrower. Shoud this be ignored (a nullop), or should it raise an exception?

State exploration is good for ensuring that an object behaves robustly in all of its high-level states. For example, a resizable Stack is explored, and the following unexpected exception is raised, when methods are exercised in the Stack's Full state:

   Stack target = new Stack();
         target.push(Object Object#1);
         target.push(Object Object#2);
         target.push(Object Object#3);
         target.push(Object Object#4);
         target.push(Object Object#5);
         target.push(Object Object#6);
      ==> ArrayIndexOutOfBoundsException#1 : EXCEPTION
showing that the Stack failed to resize itself as expected upon the sixth call to push(). The programmer may go back and fix the code so that the Stack resizes itself properly.

Validating the Test Class

Finally, you may want to construct a test oracle that learns the correct specification of the test class incrementally. In this modality, the testing tools will load an oracle data file, seeking this in the current oracle directory, which by default is the same as the test class directory (the location from which the test class was loaded). If no data file exists, a new one will be created. The only exception to this is if an oracle data file was created for the superclass of the current test class, in which case this forms the basis for the new test oracle.

When executing in the validation modality, the tools will interact with the tester to ask questions about specific test outcomes. The tester may respond with one of three signals (the input method will depend on the tool):

  • yes: to confirm the test outcome, in which case the oracle remembers this as a correct result;
  • no: to reject the test outcome, in which case the oracle remembers this as an incorrect result;
  • quit: to cancel the current test series, in which case the tools abort, saving the current results.
You will start validation at small testDepth settings, progressively increasing this as you grow confident that the results are correct. The tools are able to predict many more test results than the tester had to confirm (this is one of the main benefits of lazy systematic unit testing). Once the tool has learned a single confirmed result, it will be able to validate this, and several more outcomes, completely automatically. If the code for the test class is modified, the oracle will realise that something has changed, and will ask the tester to reconfirm just those key outcomes that appear to be different, predicting further changes where it can.

The best combination of settings to use when constructing a first test oracle is algebraic validation, since this focuses on the most important test cases describing the behaviour of the test class and presents the fewest new cases to the tester for confirmation (this may train a complete oracle, in some cases). An excerpt from the test results after training a test oracle for a Stack to a testDepth of 1 may look like this:

   Stack target = new Stack();
         target.isEmpty();
      ==> true : PASSED  (confirmed)
      
   Stack target = new Stack();
         target.size();
      ==> 0 : PASSED  (confirmed)
      
   Stack target = new Stack();
         target.pop();
      ==> EmptyStackException#1 : PASSED  (confirmed) 
         
   Stack target = new Stack();
         target.push(Object Object#1);
      ==> void : PASSED
Here, the tester has confirmed some observations on an empty Stack, but the tool has automatically passed the final operation, which returned void as the tool expected. However, the tester had to confirm the exceptional result of pop(), since the tool identifies this as unusual, or possibly unexpected. If the same test series is run again, all these results will be predicted automatically.

If the tester now switches to a different setting, such as protocol validation, the same trained test oracle will be used again, and is able to validate vastly many more test cases, for example:

   Stack target = new Stack();
         target.isEmpty();
         target.isEmpty();
      ==> true : PASSED
      
   Stack target = new Stack();
        target.isEmpty();
        target.size();
      ==> 0 : PASSED 
      
   Stack target = new Stack();
         target.isEmpty();
         target.pop();
      ==> EmptyStackException#1 : PASSED
         
   Stack target = new Stack();
         target.isEmpty();
         target.push(Object Object#1);
      ==> void : PASSED
The JWalk tools are able to determine that isEmpty() does not modify the concrete state of the Stack, so they may use the results that the tester confirmed earlier, and apply these to the longer sequences presented here. Note that the tester did not have to confirm any of these outcomes! However, at greater depths, the tools may eventually find new cases that could not be predicted from the existing oracle. For example, the following sequence:
   Stack.target = new Stack();
         target.push(Object#1);
         target.pop();
         target.push(Object#2);
         target.top();
      ==> Object#2
Must be confirmed, since the tools have never seen a Stack before containing just the top element Object#2. Typically, after confirming a only few tens of outcomes by hand (which takes comparatively little time), the tools are able to validate literally thousands of test cases. See the JWalk Publications Page for information about the tools' excellent performance.

Regent Court, 211 Portobello, Sheffield S1 4DP, United Kingdom