Advanced topics
Defining a custom system under development
Why ?
The system under development is a bridge between your fixtures and the system you are testing.
If you want to change the way is finding/instancing your fixtures, or if you need to hook the document execution, then you can define a Custom System Under Development.
To change the system under development :
Using custom types
All the examples in the documents are in strings, but fixtures want to process and return other data types.
provides a mechanism to help conversion from String to other data types and back again.
An example that looks like ... | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
... will match a class with the following methods ... public class SomeFixture { public SomeFixture(Amount amount) {...} public void setColor(RGB color) {...} public void someBool(boolean yesNo) {...} public Ratio answer() {...} } |
Lets look at how will match these.
Converters
Converters are classes implementing the interface info.novatec.testit.livingdoc.converter.TypeConverter in java.
public interface TypeConverter { boolean canConvertTo( Class type ); Object parse( String value, Class type ); String toString( Object value ); }
provides out-of-the-box type converters for the following types
- Integer
- BigDecimal
- Long
- Float
- Double
- Date
- Boolean
- Arrays
- String
or their simpler form int, long ...
The ArrayConverter calls the other converters recursively depending on the component type the array holds.
Adding and removing new type converters
Adding
The info.novatec.testit.livingdoc.LivingDoc class provides a method to add your own type converter
public static void register( TypeConverter converter)
The better place to register your custom type is in a custom system under development :
public static class CustomSystemUnderDevelopment extends DefaultSystemUnderDevelopment { public CustomSystemUnderDevelopment( String... params ) { LivingDoc.register(new MyCustomTypeConverter()); } }
The converters are always checked in a LIFO manner. If two converters can process a data type the last one that has been registered will be used. That way, you can provide your own converters in place of the standard converters.
Removing
There are two methods for unregistering added converters, In case you want to use two converters for the same data type during testing.
The info.novatec.testit.livingdoc.LivingDoc class provides these methods, too.
public static void unregisterLastAddedCustomConverter()
public static void unregisterAllCustomConverters()
These can be called from a specification or within the custom system under development.
Self conversion
Instead of registering a TypeConverter, you can use self-converting types.
Self-converting type implies that you add a static parse method to your class.
public static T parse(String val);
And then to revert back to a string,
public static String toString(T value)
Rules of conversion
From example to fixture
- First will verify if the type is can self convert (i.e. public static T parse(String) or public static T ValueOf(string))
- If not, looks for a registered TypeConverter that can handle the type.
- An UnsupportedOperationException will be thrown
From fixture return value to String
- First will verify if the type is can self revert (i.e. public static String toString(T) or public static T ToString(string))
- If not, looks for a registered TypeConverter that can handle the type.
- Use the toString() or ToString() method on the data itself.
Customizing LivingDoc fixture resolution
Finding Fixtures without imports - FixtureClass annotation
You can omit defining imports in your specs if you annotate with FixtureClass. If the spec imports no packages then LivingDoc will search for matching Fixtures by comparing the Type with all annotated fixture classes.
@FixtureClass public class BankFixture { private Bank bank; public BankFixture() { bank = new Bank(); } ... }
Changing how is finding your Fixtures
This could be useful when you are using for example an IOC or just want to add locations (packages in java) for to resolve your fixtures.
Prerequisite
To change fixture resolutions you need to define a custom system under development.
Only for specifying location to resolve fixtures (packages in java)
public static class CustomSystemUnderDevelopment extends DefaultSystemUnderDevelopment { public CustomSystemUnderDevelopment( String... params ) { super.addImport("com.mycompany.fixtures"); super.addImport("com.mycompany.specials.fixtures"); } }
By this custom system under development you tell to look in "com.mycompany.fixtures" and "com.mycompany.specials.fixtures" to resolve fixtures in specifications that your are running.
Hooking document execution
To hook a document execution, you need to define a custom system under development.
public static class CustomSystemUnderDevelopment extends DefaultSystemUnderDevelopment { public CustomSystemUnderDevelopment( String... params ) { } public void onStartDocument(Document document) { //this method is called before LivingDoc execute a document } public void onEndDocument(Document document) { //this method is called after LivingDoc has executed the document } }
Execute specifications programmatically
uses two classes to execute specifications programmatically. With this you can include LivingDoc-Tests in the Unittests of your system under develpment.
If you want to build a runnable specification you can use the new SpecificationRunnerBuilder:
// ... SpecificationRunnerBuilder builder = new SpecificationRunnerBuilder(specification.getRepository().asCmdLineOption()) .classLoader(joinClassLoader) .specificationRunnerClass(runnerClass) .sections(sectionsArray) .report(reportClass.getName()) .systemUnderDevelopment(systemUnderTest.fixtureFactoryCmdLineOption()) .withMonitor(recorderMonitor) .withMonitor(loggingMonitor) .outputDirectory(outputFile.getParentFile()); SpecificationRunner runner = builder.build(); // ...
To execute a specification you can call the run method on the runner object or you can use the SpecificationRunnerExecutor to set some optional parameters (like the output file, debug mode, locale etc.):
// ... new SpecificationRunnerExecutor(runner).execute(); SpecificationRunnerExecutor executor = new SpecificationRunnerExecutor(runner).locale(new Locale("")); // A specification runner does not have a output file. if(runner instanceof DocumentRunner) executor.outputFile(outputFile); executor.execute("ACalculatorSample.html"); // ...
How to run a specification (suite) using the command line?
Requirements
- Installed java runtime
- Your compiled fixture classes/jar (System under Test)
- Your compiled classes/jar (System under Development)
- Your specifications files
- livingdoc-cli-plugin-x.x.x.jar
- A command line tool.
Now run your local test files with the following command:
java -cp livingdoc-core-X.X.X-all.jar;path/to/systemundertest/classes;path/to/systemunderdevelopment/classes; info.novatec.testit.livingdoc.runner.Main -s /path/to/myspecs /path/to/outputresults
Configuration
How to deal with static fixture fields and the programmatically execution?
You will run into trouble if your tests are using static fields, since these are stored together with the class in the used class loader of the SpecificationBuilder. The error occurs if you are going to use the same class loader for different runners (after the first run all static fields are filled). Therefore a new option was implemented to set a custom fixture class loader. If no one is given the default class loader will be used:
// ... ClassLoader myFirstRunFixtureClassLoader = ClassUtils.toClassLoaderWithNoParent("path/to/my/fixture.jar"); ClassLoader mySecondRunFixtureClassLoader = ClassUtils.toClassLoaderWithNoParent("path/to/my/fixture.jar"); SpecificationRunnerBuilder builder = new SpecificationRunnerBuilder(specification.getRepository().asCmdLineOption()) .classLoader(joinClassLoader) .specificationRunnerClass(runnerClass) .sections(sectionsArray) .report(reportClass.getName()) .systemUnderDevelopment(systemUnderTest.fixtureFactoryCmdLineOption()) .withMonitor(recorderMonitor) .withMonitor(loggingMonitor) .outputDirectory(outputFile.getParentFile()); builder.fixtureClassLoader(myFirstRunFixtureClassLoader).execute(); builder.fixtureClassLoader(mySecondRunFixtureClassLoader).execute(); // ...
How to add a custom specification parser?
Create a DocumentBuilder
The best and fastest solution would be to create a new info.novatec.testit.livingdoc.repository.DocumentBuilder. This builder should translate your custom markup (e.g. Markdown) into HTML and in fact return a new Document build by the info.novatec.testit.livingdoc.html.HtmlDocumentBuilder. In fact: build a HtmlDocumentBuilder wrapper.
Otherwise you would be forced to create a new info.novatec.testit.livingdoc.Example class (like HTMLExample) and customize info.novatec.testit.livingdoc.document.Document to handle your custom Example class.
Here is a example how you could write your custom DocumentBuilder (using http://markdownj.org/)
// ... public class MarkdownDocumentBuilder implements DocumentBuilder { @Override public Document build(Reader reader) throws IOException { String fileContent = IOUtil.readContent( reader ); String htmlContent = new MarkdownProcessor().markdown(markup); Reader stringReader = new StringReader(htmlContent); return HtmlDocumentBuilder.tablesAndLists().build( reader ); } }
Alternative
Another option would be to let the Markup-/downDocumentBuilder only convert to HTML, therefore preventing possible confusion with library names (e.g. HTMLDocumentBuilder).
Then return it as a File type and let the Repository build it as HTML-document.
Example with the WikiMarkupDocumentBuilder:
// ... public class WikimarkupDocumentBuilder { StringWriter writer = new StringWriter(); /* MyLyn WikiText HTML-Builder */ HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer, true); MarkupParser markupParser = new MarkupParser(); public File build(File file) throws IOException { Reader reader = new FileReader(file); String html = IOUtils.toString(reader); markupParser.setMarkupLanguage(new ConfluenceLanguage()); markupParser.setBuilder(builder); markupParser.parse(html); String htmlcontent = writer.toString(); File converted = File.createTempFile("wikiconverttemp", null); Files.write(converted.toPath(), htmlcontent.getBytes()); writer.close(); converted.deleteOnExit(); return converted; } // ...
Extend the existing Repositories
All repositories are defining their supported file types. Therefore we have also to extend the repository implementations which should support our new file type/specification format.
The switch and Enum for the FileTypes has to be extend. Also you have to add the corresponding Method processing the filetype
Here is a example how you could extend the FileSystemRepository class:
// ... public Document loadDocument( String location ) throws Exception{ File file = fileAt( location ); if (!file.exists()) throw new DocumentNotFoundException( file.getAbsolutePath() ); switch (checkFileType( file )) { case HTML: return loadHtmlDocument( file ); case MARKUP: case CONFLUENCE: return loadWikimarkupDocument(file); default: throw new UnsupportedDocumentException( location ); } } // ... private Document loadWikimarkupDocument( File file ) throws IOException { return loadHtmlDocument(new WikimarkupDocumentBuilder().build(file)); } // ...
Example Enum:
//... public enum FileTypes { HTML("html"), MARKUP("markup"), CONFLUENCE("confluence"), NOTSUPPORTED("nosup"); //...
How to add aliases for an interpreter (e.g. for i18n purposes)
Aliases can be used to translate interpreter names in specifications. They are case sensitive and can contain special characters (like whitespaces) and umlauts. Whitespaces at the beginning and at the end are always removed.
An interpreter can have 1:n aliases. You can define them in a property file like the following example:
info.novatec.testit.livingdoc.interpreter.DoWithInterpreter = Ablauf, Faire avec, девать, 做 info.novatec.testit.livingdoc.interpreter.ScenarioInterpreter = Szenario, परिदृश्य, Scénario # ....
In general there are 3 ways to add an alias for an interpreter:
Programmatically (on runtime)
LivingDoc.aliasInterpreter("Scenario", ScenarioInterpreter.class); LivingDoc.aliasInterpreter("My szenario", ScenarioInterpreter.class); LivingDoc.aliasInterpreter("My szenario", "info.novatec.testit.livingdoc.interpreter.ScenarioInterpreter"); LivingDoc.aliasInterpreter("My wesome scenario!", "info.novatec.testit.livingdoc.interpreter.AClass$AInnerClass");
Edit the default aliases.properties file
This aliases.properties property file is located in the jar of livingdoc core. Using the sources this file can be found under src/main/resourecs/aliases.properties.
Provide an aliases.properties file
You can place a file named "aliases.properties" next to your livingdoc jar. By default the internal jar properties file is used as fallback.
- /myfolder/
- ...
- livingdoc-remote-agent-1.0.0-SNAPSHOT-complete.jar // Includes the livingdoc core jar
- aliases.properties
- ...
Aliases from property files will be processed only once during the first run of a specification or if the according class loader of info.novatec.testit.livingdoc.LivingDoc is unloaded (garbage collected).
Usage example
You can use the aliases in any table.
Do With | |
---|---|
... |
Can now also be used like:
Ablauf | |
---|---|
.... |