As @BNAndras mentioned, reports on test generator tool should be in separate discussion.
Report on building test generator for Groovy track
Introduction
As advised by Glenn Jackman, I downloaded canonical data for largest-series-product exercise.
These data was downloaded from:
https://github.com/exercism/exercises/largest-series-product/canonical-data.json
…to the following folder:
C:\Users\Aksima\Exercism\building-exercism\canonical-data\largest-series-product.json
But I don’t know how Glenn uses his editor’s (vim) recording feature to automate the repetitive stuff. So a decision was made to write a simple Groovy generator script for such automation.
Studying existing solutions
Before writing a program, it is a good idea to study existing solutions. Because no generator scripts was found in Groovy repository, I started searching other repositories.
It turns out that C# track have such a tooling. Generators are located in csharp/generators folder. Instructions are located in csharp/docs/GENERATORS.md file. The most interseting part in generators folder is csharp/generators/Output/Rendering folders, where one can clearly see the functions and templates used for rendering the tests.
I guess that I will try to do something like this in Groovy track.
Preparing the Groovy repository for generator development
For generator development, a “tooling” folder was created on local machine:
Aksima@DESKTOP-NIMQE87 MINGW64 ~
$ mkdir exercism/building-exercism/tooling && cd exercism/building-exercism/tooling
And a new branch:
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling
$ git clone https://github.com/Aksima/groovy.git
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling
$ cd groovy
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (main)
$ git checkout -b test-generator-tool
Implementing file structure of C# track
Now we implement a file structure close to what we see on C# track.
Creating documentation file
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (test-generator-tool)
$ echo '# Test generators' > docs/GENERATORS.md
Creating folder for generators
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (test-generator-tool)
$ mkdir generators
Saving the work
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (test-generator-tool)
$ git add .
warning: in the working copy of 'docs/GENERATORS.md', LF will be replaced by CRLF the next time Git touches it
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (test-generator-tool)
$ git commit -am "Started work on generators"
[test-generator-tool c6d9276] Started work on generators
1 file changed, 1 insertion(+)
Aksima@DESKTOP-NIMQE87 MINGW64 ~/exercism/building-exercism/tooling/groovy (test-generator-tool)
$ git push origin test-generator-tool
Next steps
The next step planned is to find learning resources on Groovy and study the language a bit. As I am is completely new in Groovy and Java and never programmed anything above “Hello, World” in these languages, it is necessary for me to learn the basics before writing a generator script.
Report on building test generator tool, part 2
Resources used
- An article on command line builder at groovy-lang.org.
- Instructions on building command-line parsers using annotations and an interface.
- Official page for Spock test framework.
- Examples of test cases built on top of Spock framework.
- Picocli official documentation.
Preparing a project for test generator tool
Creating a project
- Open IntelliJ Idea.
- Click on “New Project”.
- Set name: “TestGeneratorTool”.
- Set location, for example: “C:\Users\Aksima\Exercism\building-exercism\tooling\groovy\generators”.
- Set build system: “Gradle”.
- Set latest JDK and Groovy SDK.
- Set Gradle DSL: “Groovy”.
- Click “Create”.
Adding necessary dependencies
We will use the Spock testing framework to make sure that our command-line interface is implemented correctly.
We replace all “testImplementation” lines in build.gradle with the line highlighted on main page of Spock framework (under Install and with Gradle).
We also add implementation line for the Picocli framework. Search it under Getting Started and Add as External Dependency.
You might also need to choose a groovy implementation compatible with Spock framework, Picocli and CliBuilder. I don’t know if there exists any guidelines on that, so the best solution for you is to make decision based on googling (I did a lot of googling here!).
Removing unnecessary folders
The folders “java” and “resources”, which were autogenerated by IntelliJ Idea, probably should be manually removed from both src/main and src/test folders for consistency with existing Exercism projects, which does not contain these folders.
Create specification for command line interface
Right-click on src/test/groovy folder and choose New → Groovy Class in context menu. Name the new file CommandLineInterfaceSpec.
Add necessary import at top of file:
import spock.lang.*
Mark the new class as extending Spock’s Specifiction class.
Specifying our expectations from command-line interface
Our expectations
We expect our command-line interface to support the following options:
- -h, --help - to get help on usage;
- -i, --input=[canonical data] - to define the path to canonical-data.json for the exercise;
- -d, --repository=[repository directory path] - to define the path to directory where we downloaded the Groovy language repository.
Writing the first test case
Let us write the corresponding test cases. We start with test for ‘-h’ option:
class CommandLineInterfaceSpec extends Specification {
def "Can get help on CLI usage." () {
given: "Initialize command-line interface"
CommandLineInterface commandLineInterface = new CommandLineInterface()
and: "Parse arguments with -h flag"
OptionAccessor options = commandLineInterface.parse(["-h"])
expect: "Parsing -h flag sets the 'help' property to true."
options.getProperty("help") == true
}
}
Writing the first implementation
Obviously the test made in previous section failed, as we do not implemented this option. Let us write the implementation in CommandLineInterface.groovy file. We will use these instructions on building command-line parsers using annotations and an interface. To define the interface, a separate ICommandLineInterface.groovy file was created:
import groovy.cli.Option
import groovy.cli.Unparsed
interface ICommandLineInterface {
@Option(shortName='h', description='display usage') Boolean help()
@Unparsed() List remaining()
}
Now let us use this interface in CommandLineInterface.groovy file:
def parse(args) {
this.builder.parseFromSpec(ICommandLineInterface, args)
}
Let us look if it passes the test. And it fails…
java.lang.IllegalArgumentException: argument type mismatch
It turned out that parse
function, which was initially used, is more lenient than parseFromSpec
and can accept any iterable, for example, a Groovy list, but parseFromSpec
is more strict, expecting String...
as the type of command-line arguments. Moreover, it turned out that parse
and parseFromSpec
have different return types: parse
returns groovy.cli.picocli.OptionAccessor
instance, but parseFromSpec
returns the instance of class specified in its first argument.
So, the following fixes were made:
- The return type of
parse
method was changed toICommandLineInterface
. - In the test, we replaced
ArrayList
(["-h"]
) withString[]
(new String[] {"-h"}
) - In the test, the type of
options
variable was changed toICommandLineInterface
.
Let us run the test. And it passes now.
It the similar fashion, the tests for -i
and -d
command-line options were written and made passing.
What is next?
The next step is the parsing of canonical-data.json file.
Report on building test generator tool, part 3
Resources used
- The documentation for problem specifiations.
- The schema for canonical data.
- Groovy language official documentation.
- Parsing and producing JSON topic in Groovy language official documentation.
Parsing the canonical-data.json file
Investigating canonical-data.json file structure
The formal definition of canonical-data.json file can be found here. According to that definition, the canonical-data.json file consists of following parts:
- exercise - exercise’s slug (kebab-case) - required.
- comments - optional.
- cases - an array of labeled test items - required.
- labeledTestItem - either labeledTest or group of description and cases defined above.
- labeledTest - a single test with following properties:
- uuid - a version 4 UUID - required.
- reimplements - optional.
- description - a short, clear, one-line description - required.
- comments - optional.
- scenarios - scenarios to include/exclude test cases - optional.
- property - letters-only, lowerCamelCase property name - required.
- input - the inputs to a test case - required.
- expected - the expected return value of a test case - required.
For simplicity, we want our test generator to just generate one test class with empty test methods (without any assertions in these methods). For that, we need the following parts:
- exercise - will be used for test class name.
- cases.labeledTest.description - will be used for test method name.
- cases.labeledTest.property - as comment to what to call.
- cases.labeledTest.input - as comment to which data to test.
- cases.labeledTest.expected - expected result as comment.
Looking for tools to parse the file
Groovy language official documentation containg an interesting reading under Groovy module guides header. In particular, there are topics on Parsing and producing JSON as well on Template engines which we are going to use later.
According to Parsing and producing JSON topic, we can use JsonSlurper
for the task of parsing canonical-data.json file. Let us give it a try…
Writing the tests and code for parsing canonical data
As with command-line application, we start by defining tests which our parser must pass. For the tests, we will use an example from problem specifications documentation, rewritten in terms of Groovy’s JsonBuilder
:
@Shared String sample
def setupSpec() {
JsonBuilder builder = new JsonBuilder()
builder {
exercise 'foobar'
...
}
sample = builder.toString()
}
First we want to test if our parser can retrieve exercise’s slug:
def "Can retrieve exercise slug."() {
expect:
new CanonicalDataParser(sample).getExerciseSlug() == 'foobar'
}
Now we want to retrieve exercise slug from the contents of example JSON.
We start by importing JsonSlurper
:
import groovy.json.JsonSlurper
And then we parse with JsonSlurper
in the constructor of CanonicalDataParser
class:
LinkedHashMap specification
CanonicalDataParser(String source) {
specification = new JsonSlurper().parseText(source) as LinkedHashMap
}
After parsing, the retrieval of exercise slug is as simple as:
String getExerciseSlug() {
specification["exercise"]
}
The same job was done when writing the code for retrieval of description, property, input and description for each case in specification. To help in keeping data together, a class LabeledTestCase
was created:
class LabeledTestCase {
String description
String property
Object input
String expected
LabeledTestCase(String description, String property, Object input, String expected) {
this.description = description
this.property = property
this.input = input
this.expected = expected
}
}
We also needed to consider recursive nature of test cases, so unlike getExerciseSlug
method, the method to get cases in specification is recursive.
Next steps
The next step is to generate test class and test cases based on parsed data.
Report on building test generator tool, part 4
Resources used
- The documentation on template engines in Groovy.
Generating the test cases
Studying the documentation on template engines
The documentation on template engines is located here. It offer a lot of variants for template engines:
SimpleTemplateEngine
- for basic templatesStreamingTemplateEngine
- functionally equivalent to SimpleTemplateEngine, but can handle strings larger than 64kGStringTemplateEngine
- stores the template as writeable closures (useful for streaming scenarios)XmlTemplateEngine
- works well when the template and output are valid XMLMarkupTemplateEngine
- a very complete, optimized, template engine
After studying the available engines, it was decided that SimpleTemplateEngine
is more than enough for our purposes.
Writing test for the renderer
For our first test case for the renderer, we want a simple check: given a specification without any test methods, can we render just the test class?
This check looks like this:
class TestCasesRendererSpec extends Specification {
def "It can render an empty test class"() {
when:
JsonBuilder builder = new JsonBuilder()
builder {
exercise 'foo-bar'
cases ()
}
String sample = builder.toString()
CanonicalDataParser specification = new CanonicalDataParser(sample)
String renderedTests = TestCasesRenderer.render(specification)
then:
renderedTests == '''import spock.lang.*
class FooBarSpec extends Specification {
}'''
}
}
What we can see from the above code:
- We want to add framework import before definition of class.
- We want to define a class extending
Specification
class. It name must be in Pascal case and should end with “Spec” suffix. - After the class definition there must be curly braces, inside which we will place our test methods. For the first test case, there are no test methods, so there are nothing inside braces.
Now when test is ready, let us try to implement it:
Writing template
We want our renderer to generate output from template. First, we created templates folder at the root of the project. Second, we placed into that folder a TestClass.template file with the following content:
import spock.lang.*
class ${exerciseName}Spec extends Specification {
}
As we can see, this template already addresses a lot of points described above:
- It adds framework import before definition of class.
- It defines a class extending
Specification
class. It ends class name with “Spec” suffix. - It places curly braces after the class definition, inside which we later will place the test methods.
Writing renderer
Okay, we have a template, how can we render the template?
To render the template, we will use SimpleTemplateEngine
as described in Groovy docs:
class TestCasesRenderer {
static String render(CanonicalDataParser specification) {
String testClassPattern = Files.readString(Path.of('templates', 'TestClass.template'))
LinkedHashMap bindings = [
exerciseName: specification.exerciseSlug,
testMethods : []
]
SimpleTemplateEngine engine = new SimpleTemplateEngine()
engine.createTemplate(testClassPattern).make(bindings).toString()
}
}
In the code above, we see the following actions:
- We load template from templates\TestClass.template file.
- We create a map called
bindings
which binds values to each of the names defined in the template. - We create a simple template engine to render our template.
- And finally, we render our template with the provided bindings to string.
Okay, let us check if it passes the test:
renderedTests == '''import spock.lang.* class FooBarSpec extends Specification { }'''
| |
| false
| 3 differences (95% similarity)
| import spock.lang.*\n\nclass (f)oo(-b)arSpec extends Specification {\n}
| import spock.lang.*\n\nclass (F)oo(B-)arSpec extends Specification {\n}
import spock.lang.*
Oops, it looks like we forgot to implement conversion from kebab-case to Pascal case. Let us fix it!
Writing text case converters
For the case conversion, we will try the following approach:
- First, we get list of all words in the name to be converted, taking into account it current casing.
- Second, we convert the list of all words to the name with desired casing.
Here are the implementations for each of the steps.
First step:
static ArrayList<String> fromKebabCase(String text) {
text.split("-")
}
Second step:
static String toPascalCase(ArrayList<String> tokens) {
tokens.collect({ it[0..<1].toUpperCase() + it[1..-1].toLowerCase() }).join('')
}
After these functions were written, we can use these in creation of the class name. And the test is passing now.
Rendering test methods
In the same fashion, we wrote the test for test methods generation, the test method template and updated the TestCasesRenderer.render()
method. We made all tests passing.
Conclusion
We successfully built a test generator for Groovy track. This was quite an effort!
All three required parts are done: command-line interface, canonical data parser and test renderer. So we now have a complete and production-ready test generator for Groovy track.
The pull request for test generator tool is ready now: