T3 is a tool to automatically test a Java class.
Author: Wishnu Prasetya License: GPL v3 |
T3 is the successor of T2. The main idea is still the same as T2, however the engine has been completely revamped. T3 makes a lot of use of Java 8's closures. It also has the option to use streams, to generate test-sequences in parallel. Another major difference is that T3 offers a nicer way for the user to specify custom value generators.
To run T3 to test this class, we can do it like this:package Examples; import java.util.LinkedList; // Sorted list of integers; in ascending order. public class SimpleIntSortedList { private LinkedLists; private Integer max; public SimpleIntSortedList() { s = new LinkedList (); } public void insert(Integer x) { assert x!=null : "PRE"; int i = 0; for (Integer y : s) { if (y > x) break; i++; } s.add(i, x); // bug: should be x > max if (max == null || x < max) max = x; } // Retrieve the greatest element from the list, if it is not empty. public Integer get() { assert !s.isEmpty() : "PRE"; Integer x = max; s.remove(max); if (s.isEmpty()) max = null ; else max = s.getLast() ; assert s.isEmpty() || x >= s.getLast() : "POST"; return x; } // a class invariant private boolean classinv__() { return s.isEmpty() || s.contains(max); } }
java -ea -cp T3.jar Sequenic.T3.T3Cmd Examples.SimpleIntSortedList
T3 peforms its work in two rounds: it first generate the test suite, and then in the second round it reports the finding. To report the finding, it actually re-run the suite. The above statistic says that in the first round T3 generates a suite of 302 test-sequences. In the second round it will re-run every test-sequence it generated. By default, it is configured to stop at the first violation/error it finds. The statistic says that it re-executed two test-sequence, and found one violation (which thus must be in the 2nd sequence). None of the executed test-sequences are invalid (violating pre-conditions) or broken (fails to run, for some reason). Invalid and broken sequences are droppped during the generate-phase. However, if the SUT is non-deterministic, previously valid and unbroken test-sequence can still become bad when re-run.Suite size : 302 -- T3 generated a set of 302 test-sequences Average length : 9.364239 -- the average length of sequences, in number of steps Executed : 2 -- post-executed test-sequences, see explanation below Violating : 1 -- error found Invalid : 0 -- number of invalid test-sequences Failing : 0 -- number of failing/broken test-sequences Runtime : 9 ms
When T3 finds an error, it will print the thrown exception, and a trace of the test-sequence, as shown below. Every test-step preceeding the error will be shown, by default up to previous 10 steps.
The report below is caused by a mistake in the method insert, which incorrectly updates the internal variable max. As a result, the method get may not return the greatest element, which is detected by the post-condition assertion in the method. The violating execution is essentially:
** [step 0] CON Sequenic.T3.Examples.SimpleIntSortedList() -- calling constructor -- returned value: (Sequenic.T3.Examples.SimpleIntSortedList) @ 0 s (LinkedList) @ 1 max null ** [step 1] CALL insert on REF 0 -- calling insert(0) with (0) -- state of the receiver object: -- the state of the list is shown here: (Sequenic.T3.Examples.SimpleIntSortedList) @ 0 s (LinkedList) @ 1 [0] (Integer) : 0 max (Integer) : 0 ** [step 2] CALL insert on REF 0 -- calling insert(8) with (8) -- state of the receiver object: (Sequenic.T3.Examples.SimpleIntSortedList) @ 0 s (LinkedList) @ 1 [0] (Integer) : 0 [1] (Integer) : 8 max (Integer) : 0 -- look, max has an incorrect value ** [step 3] CALL get on REF 0 -- calling get() with () >> VIOLATION! -- a violation is reported -- state of the receiver object: (Sequenic.T3.Examples.SimpleIntSortedList) @ 0 s (LinkedList) @ 1 [0] (Integer) : 8 max (Integer) : 8 -- thrown exception: java.lang.AssertionError: POST at Sequenic.T3.Examples.SimpleIntSortedList.get(SimpleIntSortedList.java:62) ...
Now, assume CUT is testable. When testing it, T3 pretends to be a member of a client package, say P. By default, this is the package to which the CUT belongs to.The following members will be put in the testing scope:
Internally, T3 generates an ADT (Abstract Data Type) test suite, and a non-ADT suite. They are then added together to make the final test suite.
Each test-sequence in an ADT-suite starts with the creation of an instance of the CUT, and every step along the sequence affects or inspects this target instance/object. Obviously this requires that CUT can be instantiated. This can be done by calling a constructor of CUT, if there is one. T3 also looks for creator methods, which are static methods of CUT, with CUT as return type, and does not have a parameter x of type CUT.
Sequences in the non-ADT-suite focus on testing static methods and fields of CUT.
An ADT-suite consists of two sub-suites: C-suite and P-suite. A C-suite consists of sequences that start with the creation of an instance of CUT (through a constructor or a creator method), followed by a suffix. The suffix is a sequence of steps; the default length is 3. Each step can be an update to a field of the target object, or a call to a method of the target object. For each constructor or creator method which is within the testing scope, maxSamplesToCollectForEachMethod number of test sequences will be generated; the default is 100.
A P-suite consists of sequences, each has the form of c++σ++[m1,m2]++τ, where c is a step that creates the target object; σ is a prefix, and τ is a suffix. The default length of σ is 8, that of τ is 3. [m1,m2] is a pair methods from the testing scope, and m1 is a mutator. A mutator method is a method that can change the state of the target object. T3 does not currently implement a precise algorithm to identify which methods are mutators. All methods whose names do not start with "get" or "is" are assumed to be mutators. For each such pair [m1,m2], T3 will generate a bunch of P-type test sequences (so, this is basically pair-wise testing). T3 will try to distribute the sequences, so that each method in the testing scope will be covered by at least maxSamplesToCollectForEachMethod number of P-type sequences; the default is 100.
Currently there is a limit of 1000 pairs. If this number would be exceeded,
then T3 only generate sequences for these kinds of pairs:
Since it can be very hard to generate a valid sequence, there is no guarantee that each method will be covered by maxSamplesToCollectForEachMethod number of test sequences. Neither can we guarantee that, for example, each P-sequence will be a full c++σ++[m1,m2]++τ. However, T3 will strive to meet these requirements. If T3 fails to extend the sequence it currently tries to generate with a next step, it will retry a new next step for some K number of times.
Options:java -ea -cp <T3jar><:other path>* Sequenic.T3.T3Cmd <options>* <targetclass>
-co,--collection <int> Maximum size of collections and arrays that T3 generates. Default 3. -core,--core <int> To specify the number of cores. -cvg,--customvalgen <class-name> A class specifying a custom values generator. -d,--savedir <dir> Save the generated suite in this directory. -dg,--dropgood If set, non-violating traces will be dropped. -fup,--fieldupdate <float> The probability to do a field update. Default is 0.1. -help,--help Print this message. -lf,--logfile <file> To echo messages to a log-file. -ms,--msamples <float> Multiplier on number of samples to collect per goal. Default 4.0. -norc,--nooracle If set, T3 will not inject oracles in when ran in the regression mode. -obn,--objectsNesting <int> Maximum object-depth that T3 generates. Default 4. -pl,--prefixlength <int> Maximum prefix length of each sequence. Default 8. -reg,--regressionmode If set, T3 will generate a suite for regression test; the target class is assumed correct. -rs,--reportStream <file> To send report to a file. -sd,--scandir <dir> Directory to scan for classes. -sex,--showexc When set, will show the first exception throwing execution. -sl,--suffixlength <int> Maximum suffix length of each sequence. Default 3. -sqr,--seqretry <int> Maximum number that each sequence is retried. Default 5. -ss,--splitsuite <int> Split the generated suite to this number of smaller suites. Default 1. -str,--stepretry <int> Maximum number that each step is retried. Default 30. -tfop,--tfop When set, we assume to test from a different package. -to,--timeout <int> When set, specifies time out in ms. -tps,--tracePrintStream <file> To send sequence-prints to a file. -vp,--variprefix If set, T3 will generate variable legth prefixes, up to the specified maximum.
We can write a custom value generator, here is an example:package Examples; public class Person { private String name ; private String email ; private int age; private int code ; public Person(String name, String email, int age, int code) { this.name = name ; this.email = email ; this.age = age ; this.code = code ; } ... }
For any parameter (of any constructor or method) of type int or Integer, whose name is "age", the generator will randomly select from the list (17,18,60). If the parameter is named "code", it will randomly select from (-1,0,1). For other integer-types parameter, T3 will use its default generator.package Examples; import java.util.function.Function; import Sequenic.T3.Generator.Generator; import Sequenic.T3.Sequence.Datatype.*; import static Sequenic.T3.Generator.GenCombinators.* ; import static Sequenic.T3.Generator.Value.ValueMetaGeneratorsCombinators.* ; public class CustomInputGenerator { public static Generator myvalgen = FirstOf(Integer(OneOf(17,18,60)).If(hasParamName("age")), Integer(OneOf(-1,0,1)).If(hasParamName("code")), String(OneOf("anna","bob")).If(hasParamName("name")), String(OneOf("anna@gmail.com","k1@ppp")).If(hasParamName("email")) ) ; }
For the above to work, you should compile the target class with with the -parameters option enabled.
You also need to inform T3 where it can find the custom generator, using the --customvalgen option. T3 assumed the testgenerator is bound to a static field named myvalgen, as in the above example.
java -ea -cp T3.jar Sequenic.T3.T3Cmd --customvalgen Examples.CustomInputGenerator Examples.Person
which would have twice more chance to select a 0 over -1 and 1.OneOf(-1,0,0,1)
We can also define our own random selector; it needs to be a method that returns an instance of Function<Unit,T>, where T is the type of value that we want to produce. For example, to select from a list of integers, based on Gaussian distribution on the indices of the choices, we can define this:
Then we can use it, e.g. as in:static Random rnd = new java.util.Random(seed) ; static Function<Unit,Integer> GaussianOneOf(int ...s) { return unit -> { int k = rnd.nextGaussian() * s.length ; return s[k] ; } ; }
FirstOf(Integer(OneOf(17,18,60)).If(hasParamName("age")), Integer(GaussianOneOf(-1,0,1)).If(hasParamName("code")), ... )
We can also write a generator that produces objects. Suppose the class Person above has another constructor, as shown below:
Notice that it now requires an instance of Email as a parameter. T3 generates instances, but sometimes we need to have more control on the kind of instances we want to pass as inputs. A custom object generator must be a static method of this signature: O generator(int k), where O is the class whose instances we want to generate. Alternatively, a generator can also be an instance of Function<Integer,O>. Here is an example, of a custom Email generator:public Person(String name, Email email, int age, int code) { ... }
Now we need to adjust our custom value generator, to also include the above generator:static Person.Email genEmail(int k) { switch (k) { case 0 : return new Email("root") ; case 1 : return new Email("annaz") ; default : return new Email("guest") ; } }
The last line says, to use the genEmail generator, and passing to it a randomly chosen integer, taken from the list (0,1,2). The "If"-condition specifies that generator is only applied on parameters of type Email, and whose paratemers' name is "email".public static Generator myvalgen = FirstOf(Integer(OneOf(17,18,60)).If(hasParamName("age")), Integer(OneOf(-1,0,1)).If(hasParamName("code")), String(OneOf("anna","bob")).If(hasParamName("name")), String(OneOf("anna@gmail.com","k1@ppp")).If(hasParamName("email")), Apply(CustomInputGenerator.class,"genEmail",OneOf(0,1,2)).If(hasClass(Email.class).and(hasParamName("email"))) ) ; }
To run T3 using the above custom generator, we call it e.g. as below (assuming the generator is put in the class Examples.CustomInputGenerator):
java -ea -cp T3.jar Sequenic.T3.T3Cmd --customvalgen Examples.CustomInputGenerator Examples.Person
Saved test suites can be replayed. For example, suppose T3 has saved a suite in a file named Examples.SimpleIntSortedList__1393751293489.tr. To replay it we do:
This causes the test suite to be replayed. The replay will stop if a test sequence throws an exception. The -sx option will cause this sequence to be printed. Alternatively, we can use the --runall option to run the whole suite. At the end, statistic will be printed, e.g.:java -cp T3.jar -sx Sequenic.T3.ReplayCmd Examples.SimpleIntSortedList__1393751293489.tr
The general syntax to replay is:Suite size : 302 Average length : 9.427153 Executed : 302 Violating : 198 Invalid : 0 Failing : 0 Runtime : 189 ms
Options:java -cp <T3.jar><:otherpath>* Sequenic.T3.ReplayCmd [options]* <file or dir>
-b,--bulk <string> If specified the target is a directory; all suite-files there whose name is prefixed with this string are replayed. -help,--help Print this message. -lf,--logfile <file> To echo messages to a log-file. -ra,--runall If set, all sequences will be replayed; exception will not be rethrown. -rm,--regressionmode If set, and runall is false, only thrown OracleError will be rethrown. -rs,--reportStream <file> To send report to a file. -sd,--showdepth <int> If set, when on object is printed, it is printed up to this depth. Defailt=3. -sl,--showlength <int> If set will print each step in the suffix of each replayed sequence, up to this length. Default=10. -sx,--showXexec If set, then execution that throws an exception will be printed. -tps,--tracePrintStream <file> To send sequence-prints to a file.
This will generate test sequences for Examples.SimpleIntSortedList, and save them as suites in the current directory. Typically two suites will be generates: ADT_Examples.SimpleIntSortedList_timestap.tr and nonADT_Examples.SimpleIntSortedList_timestap.trSequenic.T3.T3Cmd.main("-d . Examples.SimpleIntSortedList") ;
To replay them we call:
Sequenic.T3.ReplayCmd.main("-sx -b ADT_Examples.SimpleIntSortedList . ") ; Sequenic.T3.ReplayCmd.main("-sx -b nonADT_Examples.SimpleIntSortedList . ") ;
Note that T3Cmd is used to generate test suites. If it finds a violation, it does not re-throw it. It saves the violating sequence in a suite, and proceeds with the generation of the next sequence. ReplayCmd can be used to replay a saved suite, and re-throw the first violation it finds. So, if you want to call T3 from a Junit test method, you probably want to arrange it like this:
@Test public void testC() { if(test-cases for C exists) { Sequenic.T3.ReplayCmd.main("-sx -b ADT_C . ") ; Sequenic.T3.ReplayCmd.main("-sx -b nonADT_C . ") ; } else Sequenic.T3.T3Cmd.main("-d . C") ; }
useT3() throws Exception { Config config = new Config() ; // setting T3 parameters: config.CUT = Person.class ; // target class to test config.setDirsToClasses("./bin") ; T3SuiteGenAPI tester = new T3SuiteGenAPI(null,config) ; // null means we use T3 custom value generator // generate suites: ListSS = tester.suites(true,".") ; // now, what do you want to do with SS? As an example: tester.reportCoverage(SS.get(0)); }
The generator above will generate a suite targetting the class Person. This suite will consist of maximum 500 sequences. Each sequence is generated with the above specified sequence-generator. It says that each sequence is to be like this:customSuiteGen() throws Exception { Config config = new Config() ; config.CUT = Person.class ; config.setDirsToClasses("./bin") ; // a flexible generator: CustomSuiteGenAPI sg = new CustomSuiteGenAPI(null,config) ; // null means we use T3 custom value generator sg.scope.configureForADTtesting(); // now generate a suite of 500 sequences, using the specified sequence generator: SUITE S = sg.suite(SequenceWhile( r -> ! r.isFail(), sg.create(), sg.mutatorsSegment(10), sg.instrument(), sg.method("friend"), sg.method("unfriend"), sg.nonMutatorsSegment(10) ), 500 ) ; sg.reportCoverage(S) ; }