This document describes usage of classes to create test used to generate documentation.

Approvals extension

JUnit5 extension that verify written document matches with approved one.

It checks that everything written during test is identical to the approved content.

Creating a test using ApprovalsExtension

This is an example to create a simple test using ApprovalsExtension.

You have to write a class and add register an ApprovalsExtension attribute using RegisterExtension annotation. This extension will check that everything wrote using write method has not changed since the last execution.

Test example using ApprovalsExtension
package com.github.docastest.samples.justone;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import com.github.docastest.doctesting.junitextension.ApprovalsExtension;
import com.github.docastest.doctesting.junitextension.SimpleApprovalsExtension;
public class OneTest {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void test_A() {
        doc.write("In my *test*");
    }

}

When executing test method test_A, the following text is generated.

[#com_github_docastest_samples_justone_onetest_test_a]
= Test A

In my *test*

If this content is identical to the _OneTest.test_A.approved.adoc, then the test is a success. Otherwise, test fails and the generated text is written to the _OneTest.test_A.received.adoc file. So we can compare those two files to see what has changed.

File name and title come from method name. The chapter content contains what was written using write.

Files are stored in src/test/docs/com/github/docastest/samples/justone directory which contains:

  • _OneTest.approved.adoc

  • _OneTest.test_A.approved.adoc

There is one file per test and one file for the class.

Using displayName

You can use DisplayName annotation to customize test title

Test example using DisplayName
@DisplayName("Title for the document")
class UsingDisplayNameTest {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    @DisplayName("Title for this test")
    public void test_A() {
        doc.write("In my *test*");
    }
}
Generated file with DisplayName content as title
[#com_github_docastest_doctesting_junitextension_usingdisplaynametest_test_a]
= Title for this test

In my *test*

Hide title

You can hide a test title adding the NoTitle annotation to the test. It’s usefull to reuse content of a test in different place without keeping the original structure. You also want to separate the content of one chapter through different test but display them under only title.

Test example using hiding title
public class MyTestWithoutTitleOnOneTest {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void intro() {
        doc.write("First line");
    }

    @Test
    @NoTitle
    public void my_method() {
        doc.write("My content without title");
    }

    @Test
    public void conclusion() {
        doc.write("Last line");
    }
}

The file generated from the method with NoTitle annotation has no header. On the final rendering, it’s like it was part of the previous chapter.

Rendering of the result

My test without title on one test
Intro

First line

My content without title

Conclusion

Last line

Using a custom writer

It’s possible to give a specific DocWriter to ApprovalsExtension and modify what it will be written in the final document.

Test example using ApprovalsExtension
class MyCustomWriterTest {

    @RegisterExtension
    static final ApprovalsExtension doc = new ApprovalsExtension(
            new DocWriter() {
                @Override
                public void write(String... texts) {
                    super.write(texts);
                    super.write(" // Add a comment after each call to write");
                }

                @Override
                public String formatOutput(String title, Method testMethod) {

                    return "// Add an header to the document\n"
                            + super.formatOutput(title + ": Custom title", testMethod);
                }
            }
    );

    @Test
    public void test_A() {
        doc.write("In my *test*");
    }

}

When executing test method test_A, a file _MyCustomWriterTest.test_A.approved.adoc is generated and contains the following text

// Add an header to the document

[#com_github_docastest_samples_mycustomwritertest_test_a]
= Test A: Custom title

In my *test* // Add a comment after each call to write

Using a custom formatter

It’s possible to give a specific Formatter to ApprovalsExtension and change the rendering for some instructions or create another formatter.

Test example using ApprovalsExtension
class MyCustomFormatterTest {

    @RegisterExtension
    static final ApprovalsExtension doc = new ApprovalsExtension(
            new DocWriter(
                    new AsciidocFormatter() {
                        @Override
                        /// Add the word `Warning` before the message.
                        public String warning(String message) {
                            return super.warning("Warning: " + message);
                        }
                    })
    );

    @Test
    public void test_A() {
        doc.write(doc.getFormatter().warning("My custom warning."));
    }

}

When executing test method test_A, a file _MyCustomFormatterTest.test_A.approved.adoc is generated and contains the following text

[#com_github_docastest_samples_mycustomformattertest_test_a]
= Test A


[WARNING]
====
Warning: My custom warning.
====

Nested class

Nested class can be used to organize tests. Each nested class create a nested title.

Test example using nested class
/**
 * Demo of a simple usage to generate documentation.
 */
public class DemoNestedTest {
    @RegisterExtension
    static final ApprovalsExtension writer = new SimpleApprovalsExtension();

    /**
     * Document of Addition operations.
     */
    @Nested
    class Adding {

        @Test
        @DisplayName("Adding 2 simple numbers")
        public void should_be_5_when_adding_2_and_3() {
            writer.write(String.format("%d + %d = %d", 2, 3, 2 + 3));
        }

        /**
         * A nested test.
         */
        @Test
        @DisplayName("Adding 3 simple numbers")
        public void should_be_9_when_adding_2_3_and_4() {
            writer.write(String.format("%d + %d + %d = %d", 2, 3, 4, 2 + 3 + 4));
        }
    }

    @Nested
    class Multiply {
        @Test
        @DisplayName("Multiply 2 simple numbers")
        public void should_be_12_when_multiply_4_and_3() {
            writer.write(String.format("%d * %d = %d", 4, 3, 4 * 3));
        }
    }
}

Generated files in com/github/docastest/doctesting/junitextension:

  • _DemoNestedTest.Adding.should_be_5_when_adding_2_and_3.approved.adoc

  • _DemoNestedTest.Adding.should_be_9_when_adding_2_3_and_4.approved.adoc

  • _DemoNestedTest.Multiply.should_be_12_when_multiply_4_and_3.approved.adoc

  • _DemoNestedTest.approved.adoc

Document generated
[#com_github_docastest_doctesting_junitextension_demonestedtest]
= Demo nested test

Demo of a simple usage to generate documentation.

[#com_github_docastest_doctesting_junitextension_demonestedtest_adding]
== Adding

Document of Addition operations.

include::_DemoNestedTest.Adding.should_be_5_when_adding_2_and_3.approved.adoc[leveloffset=+2]

include::_DemoNestedTest.Adding.should_be_9_when_adding_2_3_and_4.approved.adoc[leveloffset=+2]

[#com_github_docastest_doctesting_junitextension_demonestedtest_multiply]
== Multiply

include::_DemoNestedTest.Multiply.should_be_12_when_multiply_4_and_3.approved.adoc[leveloffset=+2]

Final rendering

Rendering

Demo nested test

Demo of a simple usage to generate documentation.

Adding

Document of Addition operations.

Adding 2 simple numbers

2 + 3 = 5

Adding 3 simple numbers

A nested test.

2 + 3 + 4 = 9

Multiply
Multiply 2 simple numbers

4 * 3 = 12

Document with all tests in a testclass

At the end of a test, a file is created including files generated on each test.

ApprovalsExtension must be static to be able to run AfterAll callback.

Test example used to generate class document
class MyTest {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void test_A() {
        doc.write("In my *test*");
    }

}

Class MyTest is in package com.github.docastest.samples.

Document generated
ifndef::ROOT_PATH[:ROOT_PATH: ../../../..]

[#com_github_docastest_samples_mytest]
= My test

include::_MyTest.test_A.approved.adoc[leveloffset=+1]

Final rendering

My test

Test A

In my test

Failing test output

When a test fails, the error is written in the final document. It’s help to understand and investigate on the problem.

When the test fails, the reason (exception) is written into the generated document.

Test example used to generate class document
class FailingTest {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void failing_test() {
        doc.write("Some information before failure.", "", "");
        fail("Problem on the test, it fails.");
        doc.write("Information added after failure are not in the final document.", "");
    }
}
Document generated (exception stack trace is truncated)
ifndef::ROOT_PATH[:ROOT_PATH: ../../../..]

[#com_github_docastest_samples_failingtest_failing_test]
= Failing test

Some information before failure.



*Error generating documentation*
----
org.opentest4j.AssertionFailedError: Problem on the test, it fails.
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:134)
	at com.github.docastest.samples.FailingTest.failing_test(FailingTest.java:23)
	...

----

Final rendering

Failing test

Some information before failure.

Error generating documentation

org.opentest4j.AssertionFailedError: Problem on the test, it fails.
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:134)
	at com.github.docastest.samples.FailingTest.failing_test(FailingTest.java:23)
	...

A received file is produced for the class containing the failing test. This file includes received files if they exist. It is thus possible to visualize the received file in the file with other methods.

Document generated
ifndef::ROOT_PATH[:ROOT_PATH: ../../../..]

[#com_github_docastest_samples_failingtest]
= Failing test

include::_FailingTest.failing_test.received.adoc[leveloffset=+1]

Failure report

Report when files are differents

When the received file is not the same as the approved file, first different line is displayed on report.

Received text
= Title
Description
Line from received text
Footer received
Approved text
= Title
Description
Line from approved text
Footer approved
Report
Differences between files:
    Approved: [TEMPORARY FOLDER]/files/approved.adoc
    Received: [TEMPORARY FOLDER]/files/received.adoc
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3: Line from approved text
=================================================================
3: Line from received text
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Report when files are identicals

When the received file is identical to the approved file, it’s indicated it. But this report is normally not displayed when files are identical.

Received text
= Title
Description
Approved text
= Title
Description
Report
Files are identical:
    Approved: [TEMPORARY FOLDER]/files/approved.adoc
    Received: [TEMPORARY FOLDER]/files/received.adoc

Received file shorter than approved file

When the received file is shorter than the approved file, We indicate that there is no more lines.

Received text
= Title
Description
Approved text
= Title
Description
Footer
Report
Differences between files:
    Approved: [TEMPORARY FOLDER]/files/approved.adoc
    Received: [TEMPORARY FOLDER]/files/received.adoc
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3: Footer
=================================================================
3 (no line)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Received file longer than approved file

When the received file is longer than the approved file, We indicate that there is no more lines.

Received text
= Title
Description
Footer
Approved text
= Title
Description
Report
Differences between files:
    Approved: [TEMPORARY FOLDER]/files/approved.adoc
    Received: [TEMPORARY FOLDER]/files/received.adoc
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3 (no line)
=================================================================
3: Footer
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Approved file does not exist

We indicate a specific message when approved file does not exist.

Received text
= Title
Description
Footer
Report
No approved file yet

Classes order

In order

We retrieve names of methods and nested classes in the same order they appear in source file.

Get declared methods and classes in order
Class<?> testClass = MyClass.class;
Stream<ClassesOrder.EncapsulateDeclared> declaredInOrder
        = new ClassesOrder().getDeclaredInOrder(testClass);

Only methods and classes directly under the class passed in parameter are returned.

  • method_1

  • MySubClassA

  • method_2

  • MySubClassB

  • method_3

Test example using
class MyClass {

    public void method_1() {
    }

    class MySubClassA {
        public void method_A_1() {
        }
    }

    public void method_2() {
    }

    class MySubClassB {
        public void method_B_1() {
        }
    }

    public void method_3() {
    }
}

Filter methods and classes

We can give filters to select methods and classes to return.

  • method_1

  • MySubClassB

Test example using
class MyClass {

    public void method_1() {
    }

    class MySubClassA {
        public void method_A_1() {
        }
    }

    public void method_2() {
    }

    class MySubClassB {
        public void method_B_1() {
        }
    }

    public void method_3() {
    }
}

Nested class using outer method

A nested class calling an outer method generate some special method that are not in source. Those methods should not be in sorted method list

  • methodOfClassAtFirstLevel

  • MySubClassA

Test example using
static class MyClassWithOnlySubClassExterne {

    private void methodOfClassAtFirstLevel() {
    }

    class MySubClassA {
        public void method_A_1() {
            methodOfClassAtFirstLevel();
        }
    }
}

Configuration

Available properties

  • SOURCE_PATH: Path of source code.

  • TEST_PATH: Path of test code.

  • DOC_PATH: Path where documentation is generated.

  • RESOURCE_PATH: Path of resources.

  • FORMATTER: Full qualified name of the formatter class.

Default values

By default, values are:

Key Type Value

SOURCE_PATH

Path

src/main/java

TEST_PATH

Path

src/test/java

DOC_PATH

Path

src/test/docs

RESOURCE_PATH

Path

src/test/resources

FORMATTER

Formatter

com.github.docastest.docformatter.asciidoc.AsciidocFormatter

Configuration file

You can change those folders by creating a properties file named docAsTest.properties and put it in a resource folder.

When this file contains:

SOURCE_PATH:source/java
TEST_PATH:source/test
DOC_PATH:source/docs
FORMATTER:com.github.docastest.samples.MyFormatter

Values are:

Key Type Value

SOURCE_PATH

Path

source/java

TEST_PATH

Path

source/test

DOC_PATH

Path

source/docs

RESOURCE_PATH

Path

src/test/resources

FORMATTER

Formatter

com.github.docastest.samples.MyFormatter

When config file does not exist, values are the default ones.

Document path

One of the essential points to create a documentation, is links between files. It’s even more important with documentation as test. For a java method, there is the java file, the approved and received files, and the html file. We need to easily create paths to those files. We also need to reference one file from another one either to include it or to make a link to another page.

This class helps to navigate between all that files.

Available paths from DocPath

DocPath contains the information defining the location of the item to be documented. It’s not a real path but just the location in the tree of documents. From this class, we can generate the real paths to the different kinds of associated documents.

We can create a DocPath the code below (where MyTest is declared in package com.github.docastest.samples).

Class<?> clazz = MyTest.class;
final DocPath docPath = new DocPath(clazz);
Kind of document Method called onePath.path() Description

page()

src/test/docs/com/github/docastest/samples/MyTest.adoc

File to create a page in documentation.

approved()

src/test/docs/com/github/docastest/samples/_MyTest.approved.adoc

The approved file.

received()

src/test/docs/com/github/docastest/samples/_MyTest.received.adoc

The received file.

test()

src/test/java/com/github/docastest/samples/MyTest.java

The java source file.

html()

com/github/docastest/samples/MyTest.html

The final rendered file.

Path by type

DocPath is created with this code:

final DocPath docPath = new DocPath(MyTest.class);

Note that MyTest class is declared in package com.github.docastest.samples.

We also used com.github.docastest.doctesting.utils.DocPathTest class to display from and to results with approved file (src/test/docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc).

Method Result

name()

MyTest

Code Method Result

page()

path()

src/test/docs/com/github/docastest/samples/MyTest.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

MyTest.adoc

from()

../../samples/MyTest.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

approved()

path()

src/test/docs/com/github/docastest/samples/_MyTest.approved.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

_MyTest.approved.adoc

from()

../../samples/_MyTest.approved.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

received()

path()

src/test/docs/com/github/docastest/samples/_MyTest.received.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

_MyTest.received.adoc

from()

../../samples/_MyTest.received.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

test()

path()

src/test/java/com/github/docastest/samples/MyTest.java

folder()

src/test/java/com/github/docastest/samples

filename()

MyTest.java

from()

../../../../../../java/com/github/docastest/samples/MyTest.java

to()

../../../../../docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

resource()

path()

src/test/resources/com/github/docastest/samples/MyTest.adoc

folder()

src/test/resources/com/github/docastest/samples

filename()

MyTest.adoc

from()

../../../../../../resources/com/github/docastest/samples/MyTest.adoc

to()

../../../../../docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

html()

path()

com/github/docastest/samples/MyTest.html

folder()

com/github/docastest/samples

filename()

MyTest.html

from()

../../../../../../../../com/github/docastest/samples/MyTest.html

to()

../../../../src/test/docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

Path by type with method

DocPath is created with this code:

final DocPath docPath = new DocPath(MethodReference.getMethod(MyTest::test_A));

Note that MyTest class is declared in package com.github.docastest.samples.

We also used com.github.docastest.doctesting.utils.DocPathTest class to display from and to results with approved file (src/test/docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc).

Method Result

name()

MyTest.test_A

Code Method Result

page()

path()

src/test/docs/com/github/docastest/samples/MyTest.test_A.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

MyTest.test_A.adoc

from()

../../samples/MyTest.test_A.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

approved()

path()

src/test/docs/com/github/docastest/samples/_MyTest.test_A.approved.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

_MyTest.test_A.approved.adoc

from()

../../samples/_MyTest.test_A.approved.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

received()

path()

src/test/docs/com/github/docastest/samples/_MyTest.test_A.received.adoc

folder()

src/test/docs/com/github/docastest/samples

filename()

_MyTest.test_A.received.adoc

from()

../../samples/_MyTest.test_A.received.adoc

to()

../doctesting/utils/_DocPathTest.approved.adoc

test()

path()

src/test/java/com/github/docastest/samples/MyTest.test_A.java

folder()

src/test/java/com/github/docastest/samples

filename()

MyTest.test_A.java

from()

../../../../../../java/com/github/docastest/samples/MyTest.test_A.java

to()

../../../../../docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

html()

path()

com/github/docastest/samples/MyTest.test_A.html

folder()

com/github/docastest/samples

filename()

MyTest.test_A.html

from()

../../../../../../../../com/github/docastest/samples/MyTest.test_A.html

to()

../../../../src/test/docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

Nested class

Name for nested class com.github.docastest.doctesting.sample.MyClass$MySubClass is MyClass.MySubClass.

Name for method doSomething in nested class com.github.docastest.doctesting.sample.MyClass$MySubClass is MyClass.MySubClass.doSomething.

Make path independent of operating system

Path in asciidoc files must used '/' independently of operating system and file separator.

It’s important to always generate the same reference file (.adoc) because we compare it with the last generated one. Otherwise, the test could fail when executed on another operating system.

Path path = Paths.get("src", "main", "java");
String asciiDocPath = DocPath.toAsciiDoc(path);

asciiDocPath = src/main/java

Build a path

You can create a DocPath using one of the constructor available.

With one of this code:

new DocPath(Paths.get(""), "DocPathTest")
new DocPath("DocPathTest")

Approved file is:

src/test/docs/_DocPathTest.approved.adoc

With one of this code:

new DocPath(DocPathTest.class)
new DocPath(DocPathTest.class.getPackage(), "DocPathTest")
new DocPath(Paths.get("com", "github", "docastest", "doctesting", "utils"), "DocPathTest")

Approved file is:

src/test/docs/com/github/docastest/doctesting/utils/_DocPathTest.approved.adoc

DocWriter

Usage

DocWriter is just a buffer. Everything wrote in DocWriter will be returned when asking for output. The output is composed with a title, the comment of the method (without params). An id is also added above the title to be able to apply a specific style in the chapter if needed.

Method used
public void simple_method() {

}
DocWriter usage
final DocWriter doc = new DocWriter();
doc.write(
        "Some text added to show DocWriter output.",
        "Multiple lines can be added."
);

final String output = doc.formatOutput(
        "My title",
        getClass().getMethod("simple_method")
);
Output provided
[#com_github_docastest_doctesting_utils_docwritertest_simple_method]
= My title

Some text added to show DocWriter output.
Multiple lines can be added.

Output with a class

DocWriter is also used to format output of a test class. In that case, we add a title and include all test files of the class. What is written on DocWriter is not used in this case.

Class used
public class MyTestWithTests {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void test_A() {
    }
    @Test
    public void test_B() {
    }
    @Test
    public void test_C() {
    }

}
DocWriter usage
final DocWriter doc = new DocWriter();

final Class<?> clazz = MyTestWithTests.class;
final String output = doc.formatOutput(clazz);
Output provided
[#com_github_docastest_samples_mytestwithtests]
= My test with tests

include::_MyTestWithTests.test_A.approved.adoc[leveloffset=+1]

include::_MyTestWithTests.test_B.approved.adoc[leveloffset=+1]

include::_MyTestWithTests.test_C.approved.adoc[leveloffset=+1]

Hide title

If you don’t want the default title in the generated file, add @NoTitle annotation. It can be useful when you want to include this file in another test that have its own title for example.

Test with NoTitle annotation
public class MyTestWithoutTitle {
    @Test
    @NoTitle
    public void my_method() {
        // my doc generation
    }
}
DocWriter usage
final DocWriter doc = new DocWriter();
String output = String.join("\n",
        "Some text added to show DocWriter output.",
        doc.formatOutput(
                "This title will not be displayed",
                MyTestWithoutTitle.class.getMethod("my_method")),
        "Some text added at the end."
);
Output provided
Some text added to show DocWriter output.




Some text added at the end.

Format title

When a title is specified, it is used as title.

Method testMethod = DocWriterTest.class.getMethod("format_title");
final String output = docWriter.formatOutput("My title", testMethod);
[#com_github_docastest_doctesting_utils_docwritertest_format_title]
= My title

When a title is not specified, title comes from the method name or the class name after some formatting (remove '_', uppercase first letter).

Method testMethod = DocWriterTest.class.getMethod("format_title");
final String method_output = docWriter.formatOutput(testMethod);

final String class_output = docWriter.formatOutput(MyTest.class);
Format title from method name
[#com_github_docastest_doctesting_utils_docwritertest_format_title]
= Format title
Format title from class name
[#com_github_docastest_samples_mytest]
= My test

include::_MyTest.test_A.approved.adoc[leveloffset=+1]

Add a description

A description can be added after the title using the Javadoc. It can be done with the method javadoc or the class javadoc.

Class used
/**
 * My comment for MyTestComment
 */
public class MyTestWithComment {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    /**
     * To describe a method, you can add a comment.
     * It will be added under title.
     */
    @Test
    public void test_A() {
    }

}
DocWriter usage
final DocWriter writer = new DocWriter();

final String method_output = writer.formatOutput(
        "My title",
        MyTestWithComment.class.getMethod("test_A")
);

final String class_output = writer.formatOutput(MyTestWithComment.class);
Output provided with method
[#com_github_docastest_samples_mytestwithcomment_test_a]
= My title

To describe a method, you can add a comment.
It will be added under title.
Output provided with class
[#com_github_docastest_samples_mytestwithcomment]
= My test with comment

My comment for MyTestComment

include::_MyTestWithComment.test_A.approved.adoc[leveloffset=+1]

If we want to add description of an other class (class under test for example), we can use ClassToDocument to define the class containing the description we want. It can be combine with the description on the test class.

Test class used
/**
 * Description of the test class.
 */
@ClassToDocument(clazz = ClassUnderTest.class)
public class MyTestWithClassToDocument {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    @Test
    public void test_A() {
    }

}
Class under test with description
/**
 * Description of the class ClassUnderTest.
 */
public class ClassUnderTest {
}
Output provided
[#com_github_docastest_samples_mytestwithclasstodocument]
= My test with class to document

Description of the class ClassUnderTest.

Description of the test class.

include::_MyTestWithClassToDocument.test_A.approved.adoc[leveloffset=+1]

Customize description

If you want to modify the comment before adding it to the document, you can extend the DocWriter.

Class used
/**
 * My comment for MyTestComment
 */
public class MyTestWithComment {
    @RegisterExtension
    static final ApprovalsExtension doc = new SimpleApprovalsExtension();

    /**
     * To describe a method, you can add a comment.
     * It will be added under title.
     */
    @Test
    public void test_A() {
    }

}
DocWriter usage
final DocWriter writer = new DocWriter() {
    /**
     * Override getComment to upper case the comment.
     */
    @Override
    protected Optional<String> getComment(Class classFile, Method testMethod) {
        return ((Optional<String>) super.getComment(classFile, testMethod))
                .map(c -> c.toUpperCase());
    }

};

final String method_output = writer.formatOutput(
        "My title",
        MyTestWithComment.class.getMethod("test_A")
);

final String class_output = writer.formatOutput(MyTestWithComment.class);
Output provided with method
[#com_github_docastest_samples_mytestwithcomment_test_a]
= My title

TO DESCRIBE A METHOD, YOU CAN ADD A COMMENT.
IT WILL BE ADDED UNDER TITLE.

Printer

List of codes and values they provided

We can use the following code to get values and the code used to obtain it

final List<CodeAndResult<Integer>> result = new Printer().getCodeAndResult(
        5 + 3,
        4 + 2
);

Result is

getCode() getValue()

5 + 3

8

4 + 2

6

Show result with code

We can use the following code to get values and the code used to obtain it.

final String result = new Printer().showResult(
        "abcd".substring(2),
        "abcd".substring(2, 4)
);

Result is

  • "abcd".substring(2) = cd

  • "abcd".substring(2, 4) = cd

We can use it with any type returned by code.

final String result = new Printer().showResult(
        3 + 6,
        5 + 4
);

Result is

  • 3 + 6 = 9

  • 5 + 4 = 9

Null values are displayed with null.

String value = "abcdef";
String nullValue = null;
final String result = new Printer().showResult(
        value,
        nullValue
);

Result is

  • value = abcdef

  • nullValue = null

Show result as describe with code

We can use the following code to get values and the code used to obtain it

final String result = new Printer().showResultWithFormat(
        (value, code) -> "Extracted value: `" + value + "` +\nCode to extract:\n----\n" + code + "\n----\n\n",
        "abcdef".substring(2),
        "abcdef".substring(2, 4)
);

Result is

Extracted value: cdef
Code to extract:

"abcdef".substring(2)

Extracted value: cd
Code to extract:

"abcdef".substring(2, 4)

Group by result

Description

It’s sometime interesting to group all values that provides the same result. It can be used to show several ways of doing the same thing. We can simply display result once with all cases that can be used to build it. You also can just check that we obtain the same result with different values. In that case, there is only one key in the result and you can just say that all values are the same.

Applying a function

final Map<String, List<String>> stringListMap = Printer.groupByResult(
        String::toLowerCase,
        Arrays.asList("abc", "ABC", "XyZ", "Abc")
);

You obtain a map with value returned by the function as key and a list of values that provides the key value.

abc: abc, ABC, Abc
xyz: XyZ

Using code and result class

You can create an object that associate the code and the value it return. This class provides method to help rendering data.

final CodeAndResultList<Integer> codeAndResultList = Printer.extractCodes(
        2 + 4,
        3 + 5,
        3 + 3
);

// Extract codes grouped by value.
Map<Integer, List<CodeAndResult<Integer>>> result = codeAndResultList.groupBy();

// Format an output grouping codes by value and providing a function to generate lines.
final String output = codeAndResultList.formatGroupedByValue((value, codes) ->
        "*" + value + "*: " + Printer.join(codes, code -> "`" + code.trim() + "`", ", ")
);

6: 2 + 4, 3 + 3
8: 3 + 5

Generated asciidoc in output
*6*: `2 + 4`, `3 + 3` +
*8*: `3 + 5`

You can change delimiter between values. Here, we used " / ".

codeAndResultList.formatGroupedByValue(
        (value, codes) -> "*" + value + "*: " + Printer.join(codes, code -> "`" + code.trim() + "`")
        , " / ")

6: 2 + 4,3 + 3 / 8: 3 + 5

Group by a modified value

final CodeAndResultList<String> codeAndResultList = Printer.extractCodes(
        "abc",
        "ijkl",
        "xyz",
        "a" + "b" + "c"
);

You can group all code that produce the same value.

final Map<String, List<CodeAndResult<String>>> groupedCode = codeAndResultList.groupBy();

groupedCode contains:
ijkl: "ijkl"
abc: "abc", "a" + "b" + "c"
xyz: "xyz"

If you want to group with another key, you can pass a function to build the key from the value or from the code.

final Map<Integer, List<CodeAndResult<String>>> groupedCode
        = codeAndResultList.groupBy(codeAndResult -> codeAndResult.getValue().length());

groupedCode contains:
3: "abc", "xyz", "a" + "b" + "c"
4: "ijkl"

You can group using the value after applying a function. The key used to group can be of another type than that of the initial value.

// Format an output grouping codes by value applying a function.
final String output = codeAndResultList.formatGroupedByValue(
        (value, code) -> value.length(),
        (value, codes) -> "*" + value + "*: " + Printer.join(codes, Function.identity(), ", "),
        " +\n"
);

3: "abc", "xyz", "a" + "b" + "c"
4: "ijkl"

Generated asciidoc in output
*3*: "abc", "xyz", "a" + "b" + "c" +
*4*: "ijkl"

Extract one code and result

final CodeAndResult<Integer> codeAndResultList = Printer.extractCode(
        2 + 4
);
2 + 4

Result is 6

Extract one code and result keep format

final CodeAndResult<List> codeAndResultList = Printer.extractCodeAsItWrite(
        Arrays.asList(2,
                3,
                4)
);
Arrays.asList(2,
        3,
        4)

Result is [2, 3, 4]

Simple tools

Join list to string

We provide a method to transform a list to a string applying a mapper It’s just a encapsulated call to stream, map, collect

Join with mapper

String result = Printer.join(
        Arrays.asList("abc", "efg", "ijk"),
        String::toUpperCase
);
ABC,EFG,IJK

Join with separator

String result = Printer.join(
        Arrays.asList("abc", "efg", "ijk"),
        " - "
);
abc - efg - ijk

Join with mapper and separator

String result = Printer.join(
        Arrays.asList("abc", "efg", "ijk"),
        String::toUpperCase,
        " - "
);
ABC - EFG - IJK

Svg graph

Generate an empty grid

final String svg = new SvgGraph().generate();
1 0 0 1

Specify grid size

final String svg = new SvgGraph()
        .withHeight(100)
        .withWidth(200)
        .generate();
1 0 0 1

Draw a single line

final String svg = new SvgGraph()
        .withLine("Values", Arrays.asList(0, 20, 10))
        .generate();
20 0 0 2

Draw a single line with negative values

final String svg = new SvgGraph()
        .withLine("Values", Arrays.asList(0, -20, -10))
        .generate();
0 -20 0 2

Draw a single line with negative and positive values

final String svg = new SvgGraph()
        .withLine("Values", Arrays.asList(-15, 4, -6, 30, 25))
        .generate();
30 -15 0 4

Draw a single line with float values

final String svg = new SvgGraph()
        .withLine("Values", Arrays.asList(-2.5, 1.2, -0.5, 3.6))
        .generate();
4 -3 0 3

Draw several lines

final String svg = new SvgGraph()
        .withLine("Line A", Arrays.asList(-15, 6, 30, 25))
        .withLine("Line B", Arrays.asList(5, 12, 45, 17))
        .withLine("Line C", Arrays.asList(-5, -10, -20, 8))
        .generate();
45 -20 0 3

Apply factor on axes

By default, the factor apply for axis is calculated to fill the space. This factor could be set manually. It can be use to display several graph with the same scale.

final String svg = new SvgGraph()
        .withXFactor(290) // 580 / 2 => grid width / nb values
        .withYFactor(10) // 380 / 38 => grid height / max value
        .withLine("Values", Arrays.asList(10, 38, 20)).generate();
38 0 0 2

Here, with a max value at 38 and a factor at 4, the max point is 38 * 4 = 152 This point is only 8 pixels below the height of the grid specified (160).

final String svg = new SvgGraph()
        .withHeight(160)
        .withYFactor(4)
        .withLine("Values", Arrays.asList(10, 38, 20)).generate();
38 0 0 2