diff --git a/README.md b/README.md index dafa3a1b..a52288bc 100644 --- a/README.md +++ b/README.md @@ -3,117 +3,129 @@ [![Build Status](https://api.travis-ci.org/almondtools/testrecorder.svg)](https://travis-ci.org/almondtools/testrecorder) [![codecov](https://codecov.io/gh/almondtools/testrecorder/branch/master/graph/badge.svg)](https://codecov.io/gh/almondtools/testrecorder) -__Testrecorder__ is a tool for generating test code from runnable Java code. The generated tests can then be executed with a JUnit-Runner. +__Testrecorder__ is a tool for recording runtime behavior of java programs. The results of such a recording are executable JUnit-tests replaying the recorded behavior. -* You can use these tests as part of your integration tests -* Or you can try to refactor them to make up proper unit tests +* You can use these tests as part of your characterization tests +* Or you can refactor them (they are pure java) to make up proper unit tests * Even without reusing the generated code it could give valuable insights for code understanding -It is not recommended to replace a test-driven strategy with generated-test strategy but in many cases the productive code is there and the tests are missing. In this case the first step should be to fix the current behavior by creating integration tests. This task is often hard and thankless. __Testrecorder__ can support you in this case. - -We shall start with some basics on Runtime Object Serialization. This action has a simple interface, yet it is not as powerful as test recording. Then we shall dive into the configuration of Test Recording. - -Runtime Object Serialization - the Basics -========================================= +__Testrecorder__ uses an api to serialize objects to executable java code or hamcrest matchers. + +Basic Usage +=========== + +## 1. Annotate the method to record +Annotate the method to record with `@Recorded`. For example you want to record this simple example + + public class FizzBuzz { + @Recorded + public String fizzBuzz(int i) { + if (i % 15 == 0) { + return "FizzBuzz"; + } else if (i % 3 == 0) { + return "Fizz"; + } else if (i % 5 == 0) { + return "Buzz"; + } else { + return String.valueOf(i); + } + } + } -In this section we give you an impression how code can be serialized and directly deserialized to code. The following examples will use the following `ExampleObject`: +## 2. Configure the test serialization +Write a java configuration file that implements `TestRecorderAgentConfig`. For example: - public class ExampleObject { - private String name; - - public void setName(String name) { - this.name = name; + public class AgentConfig extends DefaultTestRecorderAgentConfig { + + @Override + public SnapshotConsumer getSnapshotConsumer() { + return new ScheduledTestGenerator() + .withDumpOnShutDown(true) + .withDumpTo(Paths.get("target/generated")); } - - public String getName() { - return name; + + @Override + public long getTimeoutInMillis() { + return 100_000; + } + + @Override + public List getPackages() { + return asList("com.almondtools.testrecorder.examples"); } + } - ExampleObject exampleObject = new ExampleObject(); - exampleObject.setName("Testrecorder"); - -Serializing any Object as Java Code ------------------------------------ -Serializing an object to code is done like this: - - CodeSerializer codeSerializer = new CodeSerializer(); - String code = codeSerializer.serialize(exampleObject); +Now some explanations: -The string `code` will then contain: +`getSnapshotConsumer` should return the client that generates your test. You can use any class implementing `SnapshotConsumer` yet there are two default implementations: - ExampleObject exampleObject1 = new ExampleObject(); - exampleObject1.setName("Testrecorder"); - ExampleObject serializedObject1 = exampleObject1; +* `TestGenerator` is a low level implementation. It can collect tests but will write them only driven by API calls. Actually you should use this class only as super class for your own implementations. Such sub classes are not limited when and where to write tests. +* `ScheduledTestGenerator` is a simple `TestGenerator` implementation allowing you to specify when to write tests to the file system. As you can see in the example you can specify to write tests at program shutdown (with `withDumpOnShutDown(true)`) and you should specify the directory where serialized tests should be stored (with `withDumpTo([directory])`) +`getTimeoutInMillis` will be the limit for the recording time. This threshold is built in to skip unexpectedly long (possibly infinite) serializations. The value of `100_000` will actually only stop such long serializations. -Serializing any Object as Hamcrest Matcher Code ------------------------------------------------ -Serializing an object to matcher code is done like this: +`getPackages` should return a list of java packages that should be analyzed. Only methods in these packages are recorded (`@Recorded`-Annotations that are in packages not specified here will not have any effect) - SerializationProfile profile = new DefaultSerializationProfile(); - SerializerFacade facade = new ConfigurableSerializerFacade(profile); - DeserializerFactory factory = new ObjectToMatcherCode.Factory(); - - CodeSerializer codeSerializer = new CodeSerializer(facade, factory); - String code = codeSerializer.serialize(exampleObject); +## 3. Run your program with TestRecorderAgent +To run your program with test recording activated you have to call ist with an agent -The string `code` will then contain: +`-javaagent:testrecorder-[version]-jar-with-dependencies.jar=AgentConfig` - Matcher serializedObject1 = new GenericMatcher() { - String name = "Testrecorder"; - }.matching(ExampleObject.class); +`testrecorder-[version]-jar-with-dependencies.jar` is an artifact provided by the maven build (available in maven repository). -Test Recording - Advanced Topics -================================ +`AgentConfig` is your configuration class from the former step. -Test Recording is strictly putting together the upper code for Runtime Object Serialization. +## 4. Interact with the program and check results +You may now interact with your program and every call to a `@Recorded` method will be captured. After shutdown of your program all captured recordings will be transformed to executable JUnit tests, e.g. -How to start ------------- -The first step to Test Recording should be to instrument your code. - -- Put the test recorder jar on your class path -- You will also need the jar with dependencies -- Select one method of interest and annotate it with `@Snapshot`. Now the Testrecorder knows which method has to be recorded. -- Configure your Testrecording by writing a class `YourConfig implements SnapshotConfig` - - `getSnapshotConsumer` should return an instance of `ScheduledTestGenerator` - - `getTimeoutMillis` may be set to `100.000` - - `getPackages` should return the packages containing the classes/methods you want to record - - `getInitializer` may be set to null -- start your application with `-javaagent:testrecorder-jar-with-dependencies.jar=YourConfig` + @Test + public void testFizzBuzz0() throws Exception { + + //Arrange + FizzBuzz fizzBuzz1 = new FizzBuzz(); + + //Act + String string1 = fizzBuzz1.fizzBuzz(1); + + //Assert + assertThat(string1, equalTo("1")); + assertThat(fizzBuzz1, new GenericMatcher() { + }.matching(FizzBuzz.class)); + } -Examples --------- -Examples can be found at [testrecorder-examples](https://github.com/almondtools/testrecorder-examples) + ... + +Advanced Topics +=============== +Now that you know how testrecorder works, we will give you an introduction to more advanced topics: -Custom Serializers ------------------- -Sometimes you will encounter problems with automatic serialization because the testrecorder engine does not know the best abstraction how to serialize an object. In most times it will choose the `GenericSerializer` class, which is very generic but may contain too much of unnecessary data. +### [An Introduction to the Testrecorder Architecture](doc/Architecture.md) -If you depend on an Object that should be serialized in a special way, you can define a new `CustomSerializer implements Serializer`. Each serializer has: -- a method `getMatchingClasses` return all classes this serializer can handle -- a method `generateType` being just a factor method to create an empty serialized value -- a method `populate` being a method that is passed both the empty serialized value and the object to serialize. This should store all necessary information into the serialized value -- an inner class `Factory implements SerializerFactory` to return an instance of this serializer. +### [Tuning the Output of Testrecorder](doc/TuningOutput.md) -To enable `CustomSerializer` make it available as ServiceProvider: -- create a directory `META-INF/services` in your class path -- create a file `net.amygdalum.testrecorder.SerializerFactory` in this directory -- put the full qualified class name of `CustomSerializer$Factory` into this file +### [Recordering Input/Output with Testrecorder](doc/RecordingIO.md) +### [Using the Testrecorder-API to serialize data and generate code](doc/API.md) -Custom Deserializers (SetupGenerators, MatcherGenerators, ...) --------------------------------------------------------------- -You can also modify your output code by introducing custom deserializers. More on that in later updates. +### [Extending Testrecorder with Custom Components](doc/Extending.md) Limitations ------------ +=========== TestRecorder serialization (for values and tests) does not cover all of an objects properties. Problems might occur with: -- static fields -- synthetic fields (added by some bytecode rewriting framework) -- native state +* static fields +* synthetic fields (e.g. added by some bytecode rewriting framework) +* native state +* state that influences object access (e.g. modification counter in collections) + +Examples +======== +Examples can be found at [testrecorder-examples](https://github.com/almondtools/testrecorder-examples) + +Some additional notes ... +========================= The objective of Testrecorder is to provide an interface that is powerful, clean and extensible. To achieve this we will provide more and more configuration settings to extend the core framework. The fact that tests are generated automatically might rise wrong expectations: Testrecorder will probably always be an experts tool, meaning strong programming and debug skills are recommended to find the correct configuration and the necessary custom extensions. Testrecorder was not yet tested on a large set of code examples. Some classes are not as easy to serialize as others, so if you encounter problems, try to write an issue. Hopefully - most fixes to such problems should be solvable with custom serializers or custom deserializers. + \ No newline at end of file diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 00000000..cdef5e40 --- /dev/null +++ b/doc/API.md @@ -0,0 +1,53 @@ +Using the Testrecorder API +========================== + +##Runtime Object Serialization - the Basics + +In this section we give you an impression how code can be serialized and directly deserialized to code. The following examples will use the following `ExampleObject`: + + public class ExampleObject { + private String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + ExampleObject exampleObject = new ExampleObject(); + exampleObject.setName("Testrecorder"); + +###Serializing any Object as Java Code + +Serializing an object to code is done like this: + + CodeSerializer codeSerializer = new CodeSerializer(); + String code = codeSerializer.serialize(exampleObject); + +The string `code` will then contain: + + ExampleObject exampleObject1 = new ExampleObject(); + exampleObject1.setName("Testrecorder"); + ExampleObject serializedObject1 = exampleObject1; + + +###Serializing any Object as Hamcrest Matcher Code + +Serializing an object to matcher code is done like this: + + SerializationProfile profile = new DefaultSerializationProfile(); + SerializerFacade facade = new ConfigurableSerializerFacade(profile); + DeserializerFactory factory = new ObjectToMatcherCode.Factory(); + + CodeSerializer codeSerializer = new CodeSerializer(facade, factory); + String code = codeSerializer.serialize(exampleObject); + +The string `code` will then contain: + + Matcher serializedObject1 = new GenericMatcher() { + String name = "Testrecorder"; + }.matching(ExampleObject.class); + diff --git a/doc/Architecture.md b/doc/Architecture.md new file mode 100644 index 00000000..3b401031 --- /dev/null +++ b/doc/Architecture.md @@ -0,0 +1,4 @@ +The Architecture of Testrecorder +================================ + +TODO \ No newline at end of file diff --git a/doc/Extending.md b/doc/Extending.md new file mode 100644 index 00000000..9dbaed0a --- /dev/null +++ b/doc/Extending.md @@ -0,0 +1,50 @@ +Extending Testrecorder with Custom Components +============================================= + +Testrecorder provides some common features in the base framework. However we are aware of many features that would fit certain real problems, that cannot be solved in a generic way. So we designed Testrecorder to be extendible. + +Testrecorder could be extended in different ways: +- Custom serializers allow you to simplify the model of a recorded object +- Custom setup generators allow you to adjust the way the model is transformed to test setup code +- Custom matcher generators allow you to adjust the way the model is transformed to matcher code +- Custom intializers are needed if some instrumentations are needed before executing the agent/the test + +You can write such extensions in the workspace of your applications. Sometimes it seems better to bundle the extension to a common artifact that could be shared. There is also a section describing how to create a __testrecorder-jar-with-dependencies__ with all your custom components. + +## Custom Serializers + +The default serialization process is designed to first find the best serializer for a given object and than extract the model from it. Nulls and Primitives have special serializers, all others will default to the `GenericSerializer`. This `GenericSerializer` scan the given object with reflection and stores every field found into its model. Although with this serializer we get almost all information available to the JVM, it has some disadvantages: +- the serializer cannot see native information. Any data written to the unmanaged heap or coming from native variables cannot be reliably stored into the model +- the serializer skips some system classes because reflectively scanning such special classes is a very probable source of trouble. +- the serializer skips synthetic fields. Testrecorder itself inserts synthetic attributes into objects (which only store meta information needed for serialization), but there are also many other applications (e.g. code coverage) that insert synthetic attributes, that would pollute the serialized model. +- the serializer will see information that is part of the "transient" model. Some fields are relevant at runtime, but can (or should be) skipped for serialization. E.g. most java collection maintain a modified counter helping to identify concurrent modifications while using an iterator. This field is not relevant unless the exception has to be reproduced. + +To get around such limitations consider to write a custom serializer: + +* write a new `CustomSerializer implements Serializer`. Each serializer has: + * a method `getMatchingClasses` return all classes this serializer can handle + * a method `generateType` being just a factor method to create an empty serialized value + * a method `populate` being a method that is passed both the empty serialized value and the object to serialize. This should store all necessary information into the serialized value + * an inner class `Factory implements SerializerFactory` to return an instance of this serializer. + +To enable `CustomSerializer` make it available as ServiceProvider: +* create a directory `META-INF/services` in your class path +* create a file `net.amygdalum.testrecorder.SerializerFactory` in this directory +* put the full qualified class name of `CustomSerializer$Factory` into this file + + +## Custom Setup Generators + +TODO + +## Custom Matcher Generators + +TODO + +## Custom Initializers + +TODO (TestRecorderAgentInitializer, ServiceLoader) + +## Bundling your custom components + +TODO (Maven Shade Plugin) \ No newline at end of file diff --git a/doc/RecordingIO.md b/doc/RecordingIO.md new file mode 100644 index 00000000..7abfac3b --- /dev/null +++ b/doc/RecordingIO.md @@ -0,0 +1,11 @@ +Recording IO with Testrecorder +============================== + +## Input + +TODO (@SerializationProfile.Input) + +## Output + +TODO (@SerializationProfile.Output) + diff --git a/doc/TuningOutput.md b/doc/TuningOutput.md new file mode 100644 index 00000000..f42da2f3 --- /dev/null +++ b/doc/TuningOutput.md @@ -0,0 +1,13 @@ +Tuning Testrecorder Output +========================== + +## Hints + +### SkipChecks +TODO (not yet implemented) + +### PreferFactoryMethods +TODO (not yet implemented) + +### LoadFromFile +TODO (not yet implemented) diff --git a/src/main/java/net/amygdalum/testrecorder/ScheduledTestGenerator.java b/src/main/java/net/amygdalum/testrecorder/ScheduledTestGenerator.java index b92411d2..4b5c6584 100644 --- a/src/main/java/net/amygdalum/testrecorder/ScheduledTestGenerator.java +++ b/src/main/java/net/amygdalum/testrecorder/ScheduledTestGenerator.java @@ -5,6 +5,9 @@ import java.util.HashSet; import java.util.Set; +/** + * A configurable SnapshotConsumer client that writes tests to the file system + */ public class ScheduledTestGenerator extends TestGenerator { private static volatile Set dumpOnShutDown; @@ -24,31 +27,68 @@ public ScheduledTestGenerator() { this.path = Paths.get("."); } + /** + * specifies the path where test files are dumped + * @param path the path where test files are dumped + * @return this + */ public ScheduledTestGenerator withDumpTo(Path path) { this.path = path; return this; } + /** + * specifies how many tests will be dumped + * @param maximum the maximum of tests to be dumped + * @return this + */ public ScheduledTestGenerator withDumpMaximum(int maximum) { this.counterMaximum = maximum; return this; } + /** + * specifies the generic class name of a dumped test file. + * The argument {@code template} is a template string using characters and following template expressions: + * ${class} - the name of the class under test + * ${counter} - the number of the generated test + * ${millis} - the time stamp corresponding to the generated test + * + * @param template a template string build of characters and variable references + * @return this + */ public ScheduledTestGenerator withClassName(String template) { this.classNameTemplate = template; return this; } + /** + * specifies a time interval of latency between two test renderings. Any snapshot arriving at least this + * time interval after the last dump will trigger a new dump. + * @param timeInterval the interval of latency + * @return this + */ public ScheduledTestGenerator withDumpOnTimeInterval(long timeInterval) { this.timeInterval = timeInterval; return this; } + /** + * specifies a number of tests that should trigger a new dump. Every time the list of recorded snapshots + * exceeds this number a new dump will be triggered. + * @param counterInterval the number that will trigger a new dump + * @return this + */ public ScheduledTestGenerator withDumpOnCounterInterval(int counterInterval) { this.counterInterval = counterInterval; return this; } + /** + * specifies that all pending tests should be dumped at shutdown time + * @param shutDown true if pending tests should be dumped at shutdown, false otherwise + * @return this + */ public ScheduledTestGenerator withDumpOnShutDown(boolean shutDown) { if (shutDown) { addDumpOnShutdown();