Quantcast
Channel: Robert Wloch » C++
Viewing all articles
Browse latest Browse all 7

The Few Hours Shiny DSL Product Tutorial

0
0

Warm-up

About five years ago I was working for a customer where we had to write a lot of unit tests to test the UI. It was when I first realized, that about 80% of my effort was wasted in repeating code blocks that were just fed with different input and expectation data. Using my Xtext expertise I developed a DSL for unit tests tailored to that customer’s application. The DSL took only input data and expectation data so anybody without programming background could use it. The generator was generating Java files containing SWTbot unit tests. I had those artifacts generated directly into the source tree where they where checked in and consumed by the CI build process.

Back then one of the senior developers said it’s the frist *real* application of MDD that actually makes sense and I should blog and give a talk about it. Well, I had to leave the customer and went on to different other projects where I’ve totally forgotton about that Test DSL.

Until just recently, when I found myself in the same situation again: C# unit tests with repeating blocks of code being fed with input and expectation data. I recalled the past and built a new Test DSL that is tightly tailored to the specific scenario of testing a web service that can live on different servers. Although I cannot publish that internally used test DSL grammar here, I extended it and created a more general purpose Test DSL for sharing with the community. :-)

What you will learn in that tutorial

  • Getting started by setting up the DSL development environment (Win/Mac/Linux)
  • Creating an Xtext-based DSL project with sophisticated editor and code generator
  • Turning that DSL project into a branded product
  • Making the DSL product extendable with different custom code generators
  • Alternative DSL approaches to simplify unit testing

Before we start, I recommend you walk through the Xtext 15 Minutes Tutorial as it explains key features of the grammar which I will not do in my walkthrough.

Step 1: Getting started by setting up the DSL development environment (Win/Mac/Linux)

In case you don’t have Java installed, please install it first. You may dislike Java for whatever reasons. However, matter of fact is, there’s just no easy to use alternative when it comes to the DSL modeling tools developed around the Eclipse ecosystem. At least none, that I know of, but feel free to prove me wrong.

Next you’ll need an IDE supporting DSL development. To my knowledge the most advanced yet easy to use and stable way to go is using the Eclipse package “Java and DSL developers”. The following guide is based upon Eclipse 4.6.0 (aka Eclipse Neon). You can download ZIP archives for Windows, Mac, or Linux at the Eclipse download site.

Unzip the archive and launch Eclipse. For simplicity accept the suggested workspace path and continue with OK. Once Eclipse is loaded it presents you a Welcome screen which you’ll close for now. You can bring it up anytime via the Help menu if you feel like exploring Eclipse later on.

Step 2: Creating an Xtext-based DSL project with code generator

Create a new plain Xtext project

On the left side you’ll see the Package Explorer. From its context menu create a new project:
Create project via context menu

In the New Project wizard select a new Xtext Project:
Create Xtext project

Now the New Xtext Project wizard is shown. It contains two pages which you should fill out as shown below. Pay attention on the second wizard page to set the same check marks. Don’t choose a build system, because it’s not required for that walkthrough and may lead to various kinds of trouble (network, proxy, warnings, errors, external dependencies to name a few) distracting you from getting things done.
New Xtext Project wizard - page 1
New Xtext Project wizard - page 2

Once you’ve finished the New Xtext Project wizard you’ll find three projects in the Package Explorer and Xtext’s grammar editor is open showing the initial grammer of your DSL project.

  • The first project is “de.rowlo.testgenerator.testdsl”. Initially it contains the DSL editor’s grammar and will later also contain the domain model and code generator.
  • The second project is “de.rowlo.testgenerator.testdsl.ide”. It contains the parser for the DSL editor. That parser uses ANTLR. Xtext grammars are LR grammars with the restrictions applying to such grammars.
  • The third project is “de.rowlo.testgenerator.testdsl.ui”. It contains many extensions that add all those fancy IDE features for your DSL editor to the Eclipse RCP product.

Generate basic Xtext infrastructure

Those initial projects are just shells that need to be filled by further classes that depend on your grammar. Xtext provides a code generator that analyses the grammar file, assembles a parser and wraps it in an Eclipse based editor with all the cool IDE features such es syntax coloring, folding, code completion, outlining, and refactoring.

Before we’ll modify the initial grammer we’ll generate the Xtext artifacts in order to initialize the three projects in the Package Explorer. Most important to us is the initial code generator, that’s being created. So, from the context menu of the grammar editor invoke Run | Generate Xtext artifacts:
Generate Xtext artifacts context menu action

Please check the console view’s messages carefully. There may be an error downloading an antlr-generator-3.2.0-patch.jar file from an online location. You’ll see that error if Eclipse is not properly connected to the internet, e.g. due to proxy settings. That JAR file is required by Xtext but due to licensing issues not included with the Eclipse installation package. If, for whatever reasons, you cannot connect your Eclipse to the internet, please download that archive directly at http://download.itemis.com/antlr-generator-3.2.0-patch.jar and save it as “.antlr-generator-3.2.0-patch.jar” in the first project’s root folder ({path-to-project}/de.rowlo.testgenerator.testdsl/.antlr-generator-3.2.0-patch.jar”). Do not miss the leading period in the saved file’s name as that’s important. Files starting with ‘.’ are hidden in the Package Explorer. To make them visible use Project Explorer‘s menu:
Show hidden resources view menu action

You should see the JAR file now as shown in the following screenshot:
Name and location of downloaded itemis antlr patch

Please repeat the generation of Xtext artifacts from the grammar editor’s context menu. Xtext will not attempt to download the antlr jar anymore, as it’s already present. After the artifacts have been generated Eclipse will automatically build all projects.

Test initial project setup

The initial projects are plug-ins that can be added to an Eclipse runtime. If done so, they’ll extend the Eclipse runtime’s functionality. Of course, we want to test those plug-ins. That’s very easy with Eclipse: Just invoke Run As | Eclipse Application from the context menu of the first project in the Package Explorer.
Launch plug-ins as Eclipse application

The Eclipse IDE will launch a new runtime Eclipse IDE, configured with the very same plug-ins plus those you’ve just created in the Package Explorer. Once it’s launched create a new General | Project in the Project Explorer and name it “TestDSLs”:
Create a new general project
Create a new general project - enter project name

Within that project create a file, e.q. “Test001.testdsl”. You need to add the file extension “.testdsl” otherwise your DSL editor won’t open it automatically.
Create a new plain text file with extension testdsl

Xtext will detect that you just created a file for a DSL editor in a project that’s not set up for Xtext artifacts. As a result you’ll be asked if you want to convert your project. You should accept and probably also want Xtext to remember your decision, of course:
Accept conversion to Xtext project

Now you’ll see your opened DSL editor with an empty file. To check if it really is a DSL editor you can simply invoke code completion by pressing [Ctrl]+[Space]. Close the runtime Eclipse and let’s get back to work on the grammar and generator now.

Edit the grammar definition and generator template

Please paste the following grammar definition into your grammar editor. If you’ve chosen a different project and DSL name then you’ll need to adjust the first two lines.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
grammar de.rowlo.testgenerator.testdsl.TestDSL with org.eclipse.xtext.common.Terminals
 
generate testDSL "http://www.rowlo.de/testgenerator/testdsl/TestDSL"
 
Domainmodel:
    (elements+=AbstractElement)*;
 
AbstractElement:
    Test | Import | Type;
 
Test:
    'Test' name=QualifiedName '{' (testCases+=TestCase)* '}';
 
QualifiedName:
    ID ('.' ID)*;
 
Import:
    'import' importedNamespace=QualifiedNameWithWildcard;
 
QualifiedNameWithWildcard:
    QualifiedName '.*'?;
 
Type:
    DataType | Enum;
 
DataType:
    'datatype' name=ID;
 
Enum:
    'enum' name=ID '{'
    literals+=Literal ('|' literals+=Literal)*
    '}';
 
Literal:
    name=ID '=' value=STRING;
 
Property:
    DataTypeProperty | EnumProperty;
 
DataTypeProperty:
    type=[DataType|QualifiedName] name=ID '=' value=STRING
    ('requires' requiredEnumLiteral=[Literal|QualifiedName])?;
 
EnumProperty:
    type=[Enum|QualifiedName] name=ID '=' value=[Literal|QualifiedName]
    ('requires' requiredEnumLiteral=[Literal|QualifiedName])?;
 
TestCase:
    'TestCase' name=ID 'on' servers+=[Literal|QualifiedName] (',' servers+=[Literal|QualifiedName])*
    '{'
    input=Input
    expectation=Expectation
    '}';
 
Input:
    'tests' name='input' '{'
    ('val' properties+=Property)*
    '}';
 
Expectation:
    'verifies' name='output' '{'
    ('val' properties+=Property)*
    '}';

I’ll not explain the grammer in detail. If it is unclear, then you did not read the Xtext 15 Minutes Tutorial which I recommended to do in the beginning. What the grammar does is providing a means of specifying tests containing testcases. Testcase have two parts: Input and expectation. Both of those parts take a list of properties. The datatypes of properties may also be declared within a DSL model file following that grammar. As a bonus the grammar optionally utilizes enumeration literals to select or ignore some properties within a testcase.

You’ll see the grammar in action in a few minutes. For the moment, please save the grammar and generate the Xtext artifacts again.

The grammar is used by Xtext to create the fancy IDE editor supporting your DSL. That’s one part. The other part is generation of software artifacts from your DSL. When we had generated the Xtext artifacts a class TestDSLGenerator was created by Xtext. You probably noticed, that the file’s extension is not java but xtend. Xtend is a Java-like language and all the code translates to Java classes automatically, just have a look at the xtend-gen folder.
Write an examplary generator

The Xtend language has some build in features that assist you when specifiying code templates: Extension methods and template expressions. Please open the TestDSLGenerator and replace the content by the following generator code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/*
 * generated by Xtext 2.10.0
 */
package de.rowlo.testgenerator.testdsl.generator
 
import de.rowlo.testgenerator.testdsl.testDSL.DataTypeProperty
import de.rowlo.testgenerator.testdsl.testDSL.EnumProperty
import de.rowlo.testgenerator.testdsl.testDSL.Property
import de.rowlo.testgenerator.testdsl.testDSL.Test
import de.rowlo.testgenerator.testdsl.testDSL.TestCase
import org.eclipse.emf.common.util.EList
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.AbstractGenerator
import org.eclipse.xtext.generator.IFileSystemAccess2
import org.eclipse.xtext.generator.IGeneratorContext
 
/**
 * Generates code from your model files on save.
 * 
 * See https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#code-generation
 */
class TestDSLGenerator extends AbstractGenerator {
 
	override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
	    var allTests = resource.allContents.filter(typeof(Test))
	    allTests.forEach[test | generateUnitTestFiles(test,fsa)]
	}
 
    def generateUnitTestFiles(Test test, IFileSystemAccess2 fsa) {
        generateJavaTestArtifacts(test, fsa)
        generateCsharpTestArtifacts(test, fsa)
        generateQt5TestArtifacts(test, fsa)
    }
 
    def generateJavaTestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('java/' + test.name.toFirstUpper + '.java', pseudoJavaUnitTestContent(test))
    }
 
    def pseudoJavaUnitTestContent(Test test) {
        '''
        // generated pseudo Java unit test code
        public class «test.name.toFirstUpper» {
            «FOR testCase : test.testCases»
            @Test
            public void «testCase.name.toFirstLower»() {
                // TODO: pseudo code for setting input and asserting expectation
            }
 
            «ENDFOR»
        }
        '''
    }
 
 
    def generateCsharpTestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('c#/' + test.name.toFirstUpper + '.cs', pseudoCSharpUnitTestContent(test))
    }
 
    def pseudoCSharpUnitTestContent(Test test) {
        '''
        // generated pseudo C# unit test code
        public class «test.name.toFirstUpper» {
            «FOR testCase : test.testCases»
            [TestMethod]
            public void «testCase.name.toFirstLower»() {
                // TODO: pseudo code for setting input and checking expectation
            }
 
            «ENDFOR»
        }
        '''
    }
 
    def generateQt5TestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('qt5/' + test.name.toLowerCase + '.h', pseudoQtUnitTestHeaderContent(test))
        fsa.generateFile('qt5/' + test.name.toLowerCase + '.cpp', pseudoQtUnitTestSourceContent(test))
    }
 
    def pseudoQtUnitTestHeaderContent(Test test) {
        '''
        // generated pseudo Qt5 unit test code: header file
        #include <QtTest/QtTest>
 
        class «test.name.toFirstUpper»: public QObject
        {
            Q_OBJECT
        private slots:
            «FOR testCase : test.testCases»
            void «testCase.name.toFirstLower»();
            «ENDFOR»
        };
        '''
    }
 
    def pseudoQtUnitTestSourceContent(Test test) {
        '''
        // generated pseudo Qt5 unit test code: source file
        #include "«test.name.toLowerCase».h"
 
        «FOR testCase : test.testCases»
        void «test.name.toFirstUpper»::«testCase.name.toFirstLower»()
        {
            «IF testCase.input.properties.hasLiteral("pathA")»
            QString strA = theCallForAToTest("«testCase.inputProperty("pathA")»");
            QCOMPARE(strA, QString("«testCase.expectValue("pathA")»"));
            «ELSEIF testCase.input.properties.hasLiteral("pathB")»
            QString strB = theCallForBToTest("«testCase.inputProperty("pathB")»");
            QCOMPARE(strB, QString("«testCase.expectValue("pathB")»"));
            «ENDIF»
        }
        «ENDFOR»
 
        QTEST_MAIN(«test.name.toFirstUpper»)
        #include "«test.name.toLowerCase».moc"
        '''
    }
 
    def boolean hasLiteral(EList<Property> list, String literalName) {
        return list
                .filter(typeof(EnumProperty))
                .exists[e | e.value.name.equals(literalName)]
    }
 
 
    def String inputProperty(TestCase testCase, String literalName) {
        return propertyValue(testCase.input.properties, literalName)
    }
 
    def String expectValue(TestCase testCase, String literalName) {
        return propertyValue(testCase.expectation.properties, literalName)
    }
 
    def propertyValue(EList<Property> list, String literalName) {
        val dependentProperties = list
            .filter[p | p.requiredEnumLiteral != null && p.requiredEnumLiteral.name.equals(literalName)]
        if (dependentProperties.isNullOrEmpty) {
            return null;
        }
        val property = dependentProperties.get(0);
        return propertyValue(property);
    }
 
    dispatch def String propertyValue(DataTypeProperty property) {
        return property.value;
    }
 
    dispatch def String propertyValue(EnumProperty property) {
        return property.value.name;
    }
}

There may be errors in the project now. That’s due to the changes we’ve made to the grammer. You need to generate the Xtext artifacts again if you haven’t before changing the generator. The generator will not recognize the features of the domain model expressed by the grammer unless the domain model and respective classes have been generated for the current grammar. After the artifacts have been generated Eclipse will automatically build the plug-ins again.

Test the new grammar and generator

To test the new grammar and generator restart the Eclipse application with the changes now.
You’ll see the Project Explorer with the test project we’ve created before. That test project contains a DSL model file which does not conform to the new grammar anymore. For test purposes we’ll replace it’s content soon. But first let’s create another DSL model file called datatypes.testdsl:
Create datatypes DSL model
Its content shall be two enums and a String type declaration:

1
2
3
4
5
6
7
8
9
enum InputEnum {
	pathA = "Path A"
	| pathB = "Path B"
}
enum Servers {
	developerTestServer = "http://localhost"
	| stagingServer = "http://staging.server"
}
datatype String

You can save and close that file now.

Xtext parses automatically all testdsl files within that project (at least those being placed next to each other) and their contents are available as references in the other files. So effectivly we’ve introduced modularization here. Now, we can replace the content of the Test001.testdsl file with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Test Test001 {
	TestCase Test001_check_for_foo on Servers.developerTestServer, Servers.stagingServer
	{
		tests input {
			val InputEnum inputEnum = InputEnum.pathB
			val String inputPropertyA = "Value A" requires InputEnum.pathA
			val String inputPropertyB = "10.345" requires InputEnum.pathB
		}
		verifies output {
			val String expectValueA = "Foo" requires InputEnum.pathA
			val String expectValueB = "false" requires InputEnum.pathB
		}
	}
	TestCase Test001_check_for_bar on Servers.stagingServer
	{
		tests input {
			val String inputPropertyC = "';dropAllTables();"
		}
		verifies output {
			val String expectTableExists = "customer_data"
		}
	}
}

The DSL model is just an example. But you can see the build-in enumeration logic here: When the literal pathB is assigned to the property inputEnum the inputPropertyA and expectValueA will be ignored and vice versa.

The moment you save that file Xtext will trigger the generator. You’ll notice a new folder appearing in your project: src-gen. That’s where the generator put’s its generated artififacts. To give you a short outlook here: You can delete and create that folder manually. By doing that you can click on the “Advanced” button in the create folder dialog and link it to any place on the file system. That way, you can redirect the generated artifacts to whereever you want them to be, e.g. a source location of another project of yours.

So, let’s have a look at the src-gen folder. You should see three subfolders, which have been created by the TestDSLGenerator class. Use the context menu Open With | Text Editor to open the generated files within Eclipse and not with the default editor which may be another IDE that’s associated with the file extension. In the screenshot you can see that only inputPropertyB and the test for expectValueB was generated due to the assignment of the pathB literal.
Check the generated artifacts

Step 3: Turning that DSL project into a branded product

At that point the DSL is complete and the generator is demonstrated but needs adaption to your needs. You’ve met the two Xtext key files you need to edit in order to get your own highly sophisticated DSL editor. However, we’ve tested that DSL toolchain only as an extension to an existing Eclipse IDE. What we’re going to do now is to put our DSL toolchain into its own RCP product with its own startup splash, its own icon set, and keep only those plug-ins that are really necessary to run the DSL editor and generator.

Create a product plug-in

Turning the plug-ins into its own product means that we’re reusing essential parts of the Eclipse IDE, specifically the RCP runtime and some IDE plug-ins that Xtext utilizes for providing IDE editing experience to the generated DSL editors. But it also involves branding that product with our own splash and icon sets. We don’t want those pieces of the product configuration to be mixed in with the plug-ins we have already. So, let’s create a new Plug-in Project called de.rowlo.testgenerator.testdsl.product.
Create new Plug-in Project
New Plug-in Project wizard - page 1
New Plug-in Project wizard - page 2
Press Finished when done.

Eclipse will ask you if you want to switch to the Plug-in Development perspective. Perspectives are a central feature of Eclipse that allow you automatically show and layout different views to focus on specific tasks. In our case of that tutorial we don’t need to switch the perspective, so please say no if you’re unsure.
Don't switch perspective

After that Eclipse shows the Manifest editor with the details of the MANIFEST.MF file of the product plug-in. We’ll get back to that editor later.

Prepare the run configuration

Eclipse products are described using a product configuration file. Due to the amount of dependencies, we don’t want to edit that file from scratch but rather fill it from the current run configuration that has proven to work already. Before we can do that, we need to alter the run configuration that was automatically created by Eclipse a little bit.

Open the Run configurations dialog from the toolbar:
Prepare Run Configuration
In the Main tab change the name to TestDSL and choose to Run an application:
Prepare Run Configuration - "Main" tab

In the Plug-ins tab change the Launch with select box to plug-ins select below only and uncheck the Target Platform subtree and check the two options below the plug-ins list.
Prepare Run Configuration - "Plug-ins" tab
In the search field above the plug-ins list enter equinox. and check the equinox.ds plug-in:
Prepare Run Configuration - add org.eclipse.equinox.ds plug-in
In the filtered list also check the equinox.util plug-in:
Prepare Run Configuration - add org.eclipse.equinox.util plug-in
Now enter appl in the search field and check the ui.ide.application plug-in:
Prepare Run Configuration - add org.eclipse.ui.ide.application plug-in

Switch to the Configuration tab and check Clear the configuration area before launching. This ensures that runtime Eclipse doesn’t cache plug-in configuration which avoids occational pitfalls.
Prepare Run Configuration - "Configuration" tab

The last change needs to be done in the Common tab. Switch the radio choice to Shared file and enter the Project Explorer path to the product plug-in: “/de.rowlo.testgenerator.testdsl.product“. This will tell Eclipse to save that run configuration in a launcher file in the specified location:
Prepare Run Configuration - "Common" tab

Finally switch back to the Plug-ins tab, clear the search filter and check the checkbox Validate plug-ins automatically prior to launching. Then click several times on Add Required Plug-ins right of the plug-ins list. You can stop clicking when the number of selected plug-ins does not change no more. To check if nothing’s missing click on Validate Plug-ins. Eclipse should tell you that no problems were detected. Click on Apply and Close the dialog now.
Prepare Run Configuration - validate plug-ins

The product plug-in contains a file TestDSL.launch now. We’re not finished yet, but need to continue at the Manifest editor.

Configure a product extension

In the Manifest editor switch to the Extensions tab and click Add…:
Add new extension
In the New Extension wizard enter product in the search field and uncheck Show only extension points from the required plug-ins. Afterwards choose the org.eclipse.core.runtime.products extension point.
Select "products" extension

Eclipse will detect, that the plug-in project requires a dependency for that. So you should choose Yes:
Add required dependency
The extension is listed now and we’ll give it the ID unitTestGeneratorProduct:
Configure product extension - enter ID
Next task is to setup that extension point. We’ll start by adding a product node:
Configure product extension - add product node
And fill its parameters. The application is important and must not be changed to anything other than org.eclipse.ui.ide.workbench:
Configure product extension - set parameters of product node
Then we’ll add these properties to the product node:
Configure product extension - add appName property
Add the app16-256 PNGs as windowImages:
Configure product extension - add windowImages property
Configure product extension - add aboutImage property
Configure product extension - add aboutText property
Configure product extension - add startupForegroundColor property
Configure product extension - add startupProgressRect property
Configure product extension - add startupMessageRect property
Configure product extension - add preferenceCustomization property

The last property is setting a configuration file which is needed as it contains a startup flag to enable the progressbar on the splash image. You need to create that file in the product project’s root:
Enable progressbar on splash

Add branding resources

Download the branding_images.zip and extract it in the product project’s folder. An icons folder with the application icons and the splash.bmp will be detected by Eclipse and automatically shown in the Project Explorer. If not, please select the product plug-in and press [F5]. You can delete the ZIP archive as it’s no longer needed.

Those new resources need to be added to the binary build package otherwise they won’t be accessible at runtime. To add them go to the Build tab in the Manifest editor and select the icons folder, the splash.bmp, and the plugin_customization.ini:
Add new resources to binary build

Test prepared run configuration

Now that we’ve defined the product extension we can go back to the Run Configuration dialog and finish our adaptions by selecting the product de.rowlo.testgenerator.testdsl.product.unitTestGeneratorProduct as program to run:
Use the defined product in the launcher

Click Apply to save the changes and Run to test if the product is still launching. Check the branded Help | About dialog in the product and close the product again.
Run the launcher with the new product - branded splash
Run the launcher with the new product - branded about dialog

Add product configuration file

Now that we’ve configured the product extension and it’s launching it’s time to add a product configuration file to the product plug-in. It’s important to do this via the New Product Configuration wizard, as that allows us to choose a launch configuration as template:
Start Product Configuration wizard
Add a product configuration file using a launch configuration

Finishing the wizard will open the Product Configuration editor. In it’s Overview tab enter an ID:
Edit the product configuration - "Overview" tab
In the Launching tab you need to enter a launcher name. If left blank it defaults to eclipse or eclipse.exe depending on the target platform. Here you should also set the application icon which will be used by the operation system. The application icon type depends on your target operating system:
Edit the product configuration - "Launching" tab

In the Splash tab you need to choose the product plug-in as the one that’s providing the splash.bmp:
Edit the product configuration - "Splash" tab

Save the product configuration now.

Test the product configuration

Before testing the product configuration open all MANIFEST.MF files and check that the Vendor property in the Overview tab has the same meaningful value. Save and close the manifests.

Switch back to the Overview tab of the Product Configuration editor. In the bottom left you’ll find the Testing section. Now that you have created and configured a product configuration file you must not start your product any other way no more but using that Testing section! So: No more launching of the old run configuration. The reason is dependency management. It can become messed up if you don’t stick to that rule. Better stick to it and save you headaches later on.

Click on Launch an Eclipse application in the Testing section, to verify that the product is launchable from the product configuration:
Launch the product from the product configuration
Despite the imported launch configuration the product will have a new workspace. That’s why you don’t see the TestDSL project in the Project Explorer:
The launched branded product

However, we can import it from the other runtime workspace using the context menu’s Import… wizard on the empty Project Explorer:
Invoke Import wizard from context menu
Choose Import Project wizard

Here you need to browse for a root directory:
Import Project wizard - choose root directory

Once selected the wizard will update its projects list. Select the TestDSL project. Optionally you can check to copy it into the product’s workspace. If unchecked, the project will be referenced:
Select TestDSL project

Now you should see the DSL model files in the DSL editor and the generator should work as well. To test the generator just call from the menu Project | Clean…:
Import TestDSL project - verify if the generator is working

Export and test the product

With the successful test we’re ready to export the product to a shipable package. It’s very simple, just go to the Overview tab of the Product Configuration editor. In the bottom right you’ll find an Exporting section. Invoke the Eclipse Product export wizard:
Invoke Product Export wizard
In the wizard change the root directory from eclipse to UnitTestGenerator to complete the branding. If you miss that point, the exported application’s directory will be named “eclipse”. You also do not need the p2 repository, so uncheck it for now. Select a destination directory and export the product.
Export Unit Test Generator product as self contained RCP application
The exported product will have that folder content:
Folder structure of exported product

Invoke the UnitTestGenerator executable to see your shiny DSL product going live for the first time. Due to the IDE nature of the product you’ll be asked to choose a workspace location. Accept the defaults and optionally check not to be asked again.
Launch the Unit Test Generator RCP application - select default workspace

Once the DSL product has started the Project Explorer is empty again. Just import the TestDSL project as you just did before and verify the DSL editor and generator are working as expected.

At this point the tutorial is finished. You can find the full sources including step 4 at the github repository https://github.com/rowlo/testdsl.

Read on to learn more about making the generator modular, or to learn about alternative approaches to simplify unit testing, or to learn about trimming the DSL to make it even easier to use.

Step 4: Making the DSL product extendable with different custom code generators

The examplary implementation of the generator should not be used for production code. It was kept simple and in one piece for the purpose of the tutorial. In a real product you should turn the generator into a plug-in that extends your DSL product. In that step 4 I’ll show you how to do that.

Create an interface for the generator plug-ins

If your generators shall be plug-ins then you’ll need a common interface to simplify communication. I’ve chosen to create an ICodeGenerator interface in its own subpackage. It takes a Test model element and an IFileSystemAccess2 interface provided by Xtext:

1
2
3
4
5
6
7
8
package de.rowlo.testgenerator.testdsl.generator.codegenerator
 
import de.rowlo.testgenerator.testdsl.testDSL.Test
import org.eclipse.xtext.generator.IFileSystemAccess2
 
interface ICodeGenerator {
    def void generateTestArtifacts(Test test, IFileSystemAccess2 fsa);
}

Create an extension point for generators

Next we’ll have the de.rowlo.testgenerator.testdsl project declare a new extension point that can be implemented by other plug-ins. To do that you open the Manifest edior of that project and switch to the Extension Points tab. Add a codeGenerator extension point as shown below:
Add codeGenerator extension point

When you finish the New Extension Point wizard the schema for that extension point is opened in the Extension Point Schema editor. In its Overview tab enter a meaningful description:
Extension Point Schema editor - "Overview" tab

In the Definition tab first add an element named codegenerator:
Extension Point Schema editor - add element "codegenerator"

Add attributes class and name to the codegenerator element:
Extension Point Schema editor - add attribute "class"
Extension Point Schema editor - add attribute "name"

Now select the first node extension and add a new choice to it. Also from the context menu of the choice add a codegenerator element reference to the choice:
Extension Point Schema editor - add "choice"
Extension Point Schema editor - add "codegenerator" reference

Congratulations: You’ve just made the DSL product extentable by plug-ins providing implementations of the ICodeGenerator interface.

Create an extensions manager class for instantiating generator plug-ins

Unfortunately, declaring the extension point is not enough. The product needs to learn about the provided plug-ins when starting. That is done using an extensions manager. Plug-ins are detected by the equinox runtime. In order to utilize that plug-in mechnism we need to add some plug-in dependencies. Open the Manifest edior, switch to the MANIFEST.MF tab, and add from the following list what’s missing:

1
2
3
4
5
6
7
8
9
10
11
12
Require-Bundle: org.eclipse.xtext,
 org.eclipse.xtext.xbase,
 org.eclipse.equinox.registry,
 org.eclipse.equinox.common;bundle-version="3.5.0",
 org.eclipse.emf.ecore,
 org.eclipse.xtext.xbase.lib,
 org.antlr.runtime,
 org.eclipse.xtext.util,
 org.eclipse.xtend.lib,
 org.eclipse.emf.common,
 org.eclipse.e4.core.di;bundle-version="1.6.0",
 org.eclipse.core.runtime;bundle-version="3.12.0"

The project is now ready for the CodeGeneratorExtensionsManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package de.rowlo.testgenerator.testdsl.generator.codegenerator;
 
import java.util.HashMap;
import java.util.Map;
 
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.e4.core.di.annotations.Execute;
 
public class CodeGeneratorExtensionsManager {
	private static final String CODEGENERATOR_ID = "de.rowlo.testgenerator.testdsl.codeGenerator";
	private static final Map<String, ICodeGenerator> codeGenerators = new HashMap<String, ICodeGenerator>();
 
	@Execute
	public void execute(IExtensionRegistry registry) {
		IConfigurationElement[] config = registry.getConfigurationElementsFor(CODEGENERATOR_ID);
		try {
			for (IConfigurationElement e : config) {
				final Object o = e.createExecutableExtension("class");
				if (o instanceof ICodeGenerator) {
					ICodeGenerator codeGenerator = (ICodeGenerator) o;
					String codeGeneratorName = e.getAttribute("name");
					if (codeGeneratorName == null || codeGeneratorName.isEmpty()) continue;
					codeGenerators.put(codeGeneratorName, codeGenerator);
				}
			}
		} catch (CoreException ex) {
		}
	}
 
	public static Map<String, ICodeGenerator> getRegisteredCodeGenerators() {
		if (codeGenerators.isEmpty()) {
			IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
			CodeGeneratorExtensionsManager extensionsManager = new CodeGeneratorExtensionsManager();
			extensionsManager.execute(extensionRegistry);
		}
		return new HashMap<String, ICodeGenerator>(codeGenerators);
	}
}

Using dependency injection the execute method is called with an extension registry at product startup time. The method looks for plug-ins satisfying the codeGenerator extension point and stores instances in a map. That map can be retrieved later on, e.g by the TestDSLGenerator class.

Refactor the TestDSLGenerator class

The TestDSLGenerator class will be replaced by a very generic code which doesn’t generate any artifacts itself. Instead, it’ll get the map of plugged in generators and call each one with the Test model element:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
 * generated by Xtext 2.10.0
 */
package de.rowlo.testgenerator.testdsl.generator
 
import de.rowlo.testgenerator.testdsl.generator.codegenerator.CodeGeneratorExtensionsManager
import de.rowlo.testgenerator.testdsl.testDSL.Test
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.AbstractGenerator
import org.eclipse.xtext.generator.IFileSystemAccess2
import org.eclipse.xtext.generator.IGeneratorContext
 
/**
 * Generates code from your model files on save.
 * 
 * See https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#code-generation
 */
class TestDSLGenerator extends AbstractGenerator {
 
	override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
		var allTests = resource.allContents.filter(typeof(Test))
		allTests.forEach[test | generateUnitTestFiles(test,fsa)]
	}
 
	def generateUnitTestFiles(Test test, IFileSystemAccess2 fsa) {
		val codeGenerators = CodeGeneratorExtensionsManager.registeredCodeGenerators.values
		for (codeGenerator : codeGenerators) {
			codeGenerator.generateTestArtifacts(test, fsa)
		}
	}
 
}

The code that used to generate the pseudo codes for Java, C#, and Qt5 is moved to three separate plug-in projects:
Three plug-in projects, one for each generator

Let’s create those three plug-in projects. We’ll start with the C# generator. On the second page of the New Plug-in Project wizard set these values and Finish the wizard:
Create plug-in project for C# generator

The Manifest editor will open where the Extensions tab is the place to go. Add a codeGenerator extension:
Add codeGenerator extension to C# generator plug-in

Now create a package de.rowlo.testgenerator.testdsl.codegenerator and an Xtend class ExampleCsharpCodeGenerator in it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package de.rowlo.testgenerator.testdsl.codegenerator
 
import de.rowlo.testgenerator.testdsl.generator.codegenerator.ICodeGenerator
import de.rowlo.testgenerator.testdsl.testDSL.Test
import org.eclipse.xtext.generator.IFileSystemAccess2
 
class ExampleCsharpCodeGenerator implements ICodeGenerator {
 
    override generateTestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('c#/' + test.name.toFirstUpper + '.cs', pseudoCSharpUnitTestContent(test))
    }
 
    def pseudoCSharpUnitTestContent(Test test) {
        '''
        // generated pseudo C# unit test code
        public class «test.name.toFirstUpper» {
            «FOR testCase : test.testCases»
            [TestMethod]
            public void «testCase.name.toFirstLower»() {
                // TODO: pseudo code for setting input and checking expectation
            }
 
            «ENDFOR»
        }
        '''
    }
}

Now add a codegenerator element to the codeGenerator extension in the Manifest editor and set the class to de.rowlo.testgenerator.testdsl.codegenerator.ExampleCsharpCodeGenerator.
Add "codegenerator" element to extension point referencing the C# generator class

That’s it for the C# generator.

Now let’s do the Java generator. Proceed as above but use Java where C# or Csharp was used before. The Xtend class should look like that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package de.rowlo.testgenerator.testdsl.codegenerator
 
import de.rowlo.testgenerator.testdsl.generator.codegenerator.ICodeGenerator
import de.rowlo.testgenerator.testdsl.testDSL.Test
import org.eclipse.xtext.generator.IFileSystemAccess2
 
class ExampleJavaCodeGenerator implements ICodeGenerator {
 
    override generateTestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('java/' + test.name.toFirstUpper + '.java', pseudoJavaUnitTestContent(test))
    }
 
    def pseudoJavaUnitTestContent(Test test) {
        '''
        // generated pseudo Java unit test code
        public class «test.name.toFirstUpper» {
            «FOR testCase : test.testCases»
            @Test
            public void «testCase.name.toFirstLower»() {
                // TODO: pseudo code for setting input and asserting expectation
            }
 
            «ENDFOR»
        }
        '''
    }
 
}

Finally, create the third generator plug-in for the Qt5 code. Replace every use of Java with Qt5. The Xtend generator class should look like that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package de.rowlo.testgenerator.testdsl.codegenerator
 
import de.rowlo.testgenerator.testdsl.generator.codegenerator.ICodeGenerator
import de.rowlo.testgenerator.testdsl.testDSL.DataTypeProperty
import de.rowlo.testgenerator.testdsl.testDSL.EnumProperty
import de.rowlo.testgenerator.testdsl.testDSL.Property
import de.rowlo.testgenerator.testdsl.testDSL.Test
import de.rowlo.testgenerator.testdsl.testDSL.TestCase
import org.eclipse.emf.common.util.EList
import org.eclipse.xtext.generator.IFileSystemAccess2
 
/**
 * That generator is dependend on a concrete instance of a domain model.
 * Specifically it's looking for enum literals "LITERAL_A" and "LITERAL_B".
 */
class ExampleQt5CodeGenerator implements ICodeGenerator {
 
    override generateTestArtifacts(Test test, IFileSystemAccess2 fsa) {
        fsa.generateFile('qt5/' + test.name.toLowerCase + '.h', pseudoQtUnitTestHeaderContent(test))
        fsa.generateFile('qt5/' + test.name.toLowerCase + '.cpp', pseudoQtUnitTestSourceContent(test))
    }
 
    def pseudoQtUnitTestHeaderContent(Test test) {
        '''
        // generated pseudo Qt5 unit test code: header file
        #include <QtTest/QtTest>
 
        class «test.name.toFirstUpper»: public QObject
        {
            Q_OBJECT
        private slots:
            «FOR testCase : test.testCases»
            void «testCase.name.toFirstLower»();
            «ENDFOR»
        };
        '''
    }
 
    def pseudoQtUnitTestSourceContent(Test test) {
        '''
        // generated pseudo Qt5 unit test code: source file
        #include "«test.name.toLowerCase».h"
 
        «FOR testCase : test.testCases»
        void «test.name.toFirstUpper»::«testCase.name.toFirstLower»()
        {
            «IF testCase.input.properties.hasLiteral("pathA")»
            QString strA = theCallForAToTest("«testCase.inputProperty("pathA")»");
            QCOMPARE(strA, QString("«testCase.expectValue("pathA")»"));
            «ELSEIF testCase.input.properties.hasLiteral("pathB")»
            QString strB = theCallForBToTest("«testCase.inputProperty("pathB")»");
            QCOMPARE(strB, QString("«testCase.expectValue("pathB")»"));
            «ENDIF»
        }
        «ENDFOR»
 
        QTEST_MAIN(«test.name.toFirstUpper»)
        #include "«test.name.toLowerCase».moc"
        '''
    }
 
    def boolean hasLiteral(EList<Property> list, String literalName) {
        return list
                .filter(typeof(EnumProperty))
                .exists[e | e.value.name.equals(literalName)]
    }
 
 
    def String inputProperty(TestCase testCase, String literalName) {
        return propertyValue(testCase.input.properties, literalName)
    }
 
    def String expectValue(TestCase testCase, String literalName) {
        return propertyValue(testCase.expectation.properties, literalName)
    }
 
    def propertyValue(EList<Property> list, String literalName) {
        val dependentProperties = list
            .filter[p | p.requiredEnumLiteral != null && p.requiredEnumLiteral.name.equals(literalName)]
        if (dependentProperties.isNullOrEmpty) {
            return null;
        }
        val property = dependentProperties.get(0);
        return propertyValue(property);
    }
 
    dispatch def String propertyValue(DataTypeProperty property) {
        return property.value;
    }
 
    dispatch def String propertyValue(EnumProperty property) {
        return property.value.name;
    }
 
}

Update and test the product

Those three new generator plug-ins are yet unknown to the product configuration. Therefore let’s open the UnitTestGenerator.product file and switch to the Contents tab. Here we’ll add the three new plug-ins:
Add generator plug-ins to product configuration

Save the product configuration, switch to the Overview tab and test launch the product. When the product is started, just clean the TestDSLs project and check if the artifact will be generated again.

A note for the interested and advanced coders: In fact it wouldn’t be necessary to update the product configuration as the product will work without generators now. You could also export those three generator plug-ins as jars and put them into the plugins folder of the product. When restarting the product, they’ll be recognized.

Congratulations: You’ve learned how to add generators as a plug-in to your product.

Alternative DSL approaches to simplify unit testing

Wait! Your Test DSL is such a new thing! No it probably isn’t. However, doing a quick search for “unit test DSL domain specific language” on the internet there was not a single first page result that actually featured a *real* DSL.

The definition of DSL according to Wikipedia reads:

A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains.

Strictly interpreting that definition excludes the so-called internal DSLs as they are no computer language itself but are merely a result of clever API design of a GPL. I think such solutions should rather be referred to as Domain Specific API.

The most promising alternatives I found during a quick search are Domain Specific APIs featuring Fluent Interfaces:
- “Clean Code: Writing Readable Unit Tests with Domain Specific Languages”
The language used is still C#.
- “Turning Assertions Into a Domain-Specific Language”
The language used is still Java.
- “Writing Clean Tests – Replace Assertions with a Domain-Specific Language”
The language used is still Java.
- “Learning to write DSLs utilities for unit tests and am worried about extensablity”
The language used is still Java.
- “NaturalSpec”
The language used is F# although that API doesn’t look like programming no more. Intriguing! Here’s a concise overview.

Bonus: How to make Test DSL not look like programming at all

So now, let’s strip the generic Test DSL down to what your grandparents can handle. Wait, what? It’s already simple you may say. Well, we’re using data types. That’s programming blabla and not test data domain. So let’s get rid of it and put the cleverness into the generators.

We change the AbstractElement rule to:

1
2
AbstractElement:
    Test | Import | Enum;

We remove the Type, and DataType rules completely.

We modify the DataTypeProperty rule to:

1
2
3
DataTypeProperty:
    name=ID '=' value=STRING
    ('requires' requiredEnumLiteral=[Literal|QualifiedName])?;

Simple as that. Regenerate Xtext artifacts and open the UnitTestGenerator.product file and test launch the product. Of course, the changes to the DSL’s grammar invalidate the models we’d created before.
But the impact is little. In the datatypes.testdsl just remove the “datatype String” line. In the Test001.testdsl file remove all “String ” occurances.
Stripped down DSL

You can go even further by changing the EnumProperty rule to:

1
2
3
EnumProperty:
    name=ID '=' value=[Literal|QualifiedName]
    ('requires' requiredEnumLiteral=[Literal|QualifiedName])?;

Now users don’t get to see any type name in the DSL editor but only typed values with the only exception being the fully qualified names of enum literals. However, I suppose one can hook into Xtext at some place and have the editor render the literal description instead of the value. Feedback regarding that is welcome!

Thanks for reading and I hope this tutorial brings you even further.


Viewing all articles
Browse latest Browse all 7

Latest Images

Trending Articles





Latest Images