Feature Implementation - concept

[Document WIP]

Feature declaration and testing.

Model

Definition

Each feature in a separate *.xml file that is readable as Composite (see model above).

// BeanExpander.globalCounter.xml
<feature name="csvImport">
  <expander name="BeanExpander" packageName="net.democritus.expander.ejb3.dataElement"/>
  <mapping><![CDATA
    <mapping xmlns="http://nsx.normalizedsystems.org/201806/expanders/mapping">
      <let name="option" eval="dataElement.getOption('isCsvImportEnabled')"/>
      <value name="isCsvImportEnabled" eval="option.defined"/>
      <conditionalValue name="csvImportImplementation">
        <option if="!option.value.empty" eval="classBuilder.on(option.value)"/>
        <option if="true" eval="classBuilder.on(dataElement, 'CsvImporterImpl')"/>
      </conditionalValue>
    </mapping>
  ]]></mapping>
  <template>
    <templateType name="STG"/>
    <content><![CDATA[
base() ::= <<
<@feature:(name : csvImport, if : isCsvImportEnabled):start>
<@imports:start>
import net.democritus.upload.ImportFile;
import net.democritus.upload.ImportResult;
import <csvImportImplementation.qualifiedName>;
<@imports:end>
<@methods:(group : "Import / Export"):start>
@Override
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public ImportResult importFile(ParameterContext\<ImportFile\> parameter) {
  String type = parameter.getValue().getType();
  if ("csv".equals(type)) {
    return new <csvImportImplementationClass.className>().importFile(parameter);
  // anchor:custom-import-implementations:start
  // anchor:custom-import-implementations:end
  } else {
    return ImportResult.globalError("Unsupported import type (" + type + ")");
  }
}
<@methods:end>
<@feature:end>
>>
  ]]></content>
  </template>
</feature>

To be addressed:

  • Registration mechanism
    • predefined locations where to look for Feature XMLs
  • Generating mapping overview?
    • - Mapping is spread out over many files
      • this is already a potential problem with <let>
      • we can generate a combined mapping as a preview
  • Multiple languages in a single file
    • XML as an interchange
    • with Intellij language injections not a major issue
    • proper formats can be added later, but that will need a import/export processor between the "proper" format and the XML
  • Mapping issues
    • ? clashes -- Two features using the same key for different purposes
      • can be fixed with implicitly grouping feature-specific mapping and then de-grouping when rendering the feature template
    • duplication -- Two features needing the same data, but define them in different way
  • - Hard to get overview
    • + However as features are being reified and the mapping can be parsed, we can build tools to provide much better overview
    • A mechanism to easily see what features are to be applied and from where

Testing

If we can define Expanders without Java, we should be able to test it without Java.

Thorough testing of conditional paths and variations should be straightforward.

Mapping is currently not tested, but should be.

Ability to assess how well is tested the mapping, features, and template itself.

Typical Expander test ought to consist of three parts:

  1. model setup (instantiating composites and contexts)
    • previous
      • Descriptor Syntax
      • Helper methods / prepared scenarios
    • next
      • XML reading
      • new Composite builder syntax [WIP]
      • hand-modified/constructed composites/context
      • prepared scenarios
  2. mapping testing
    • correct mapping is a precondition for correct template, easier to unit test if separated from STG
    • previous
      • N/A
    • next
      • ArtifactExpander exposes getModel()
  3. result testing
    • previous
      • String concat of code
      • Existence of a string (e.g. method signature) in the entire template and/or insertion
      • lack of structure
    • next
      • test STG containing the full result
        • split into base (without anchors), and individual anchors
      • tests based on the features

Similar to the Feature definition, parts of the <featureTest> should be split among more files, but XML is a good base with Intellij.

Sketch of a feature XML test file:

// BeanExpander.globalCounter.test.xml
<featureTest>
  <feature name="globalCounter" expander="BeanExpander"  packageName="net.democritus.expander.ejb3.dataElement"/>
  <baseModel>
    dataElement X {
    }
  </baseModel>
  <mappingTests>
    <mappingTest>
      <given>
        globalOptionSettings {
          counterDefault = "IDENTITY"
        }
      </given>
      <expect>
        { "useGlobalIdCounter" : false }
      </expect>
    </mappingTest>
  </mappingTests>
  <templateTests>
    <templateTest>
      <mapping>
        { "useGlobalIdCounter" : true }
      </mapping>
      <expect><![CDATA[
      @imports
      import net.democritus.sys.IdCounterLocal;
      @/imports

      @variables
      @EJB private IdCounterLocal idCounterLocal;
      @/variables
      ]]></expect>
    </temlateTest>
  </templateTests>
</featureTest>

Expanded Tests

Moving the tests from Java to a higher representation creates extra mental gap and requires extra runtime processors to execute them.

Engine-less approach: expand tests -- any error should be the same as if the test was written by hand by a programmer.

Tests definition:

<featureTest>
  <feature name="globalCounter" expander="BeanExpander"  packageName="net.democritus.expander.ejb3.dataElement"/>
  <baseModel>
    dataElement X {
    }
  </baseModel>
  <mappingTests>
    <mappingTest>
      <given>
        globalOptionSettings {
          counterDefault = "IDENTITY"
        }
      </given>
      <expect>
        { "useGlobalIdCounter" : false }
      </expect>
    </mappingTest>
  </mappingTests>
  <templateTests>
    <templateTest>
      <mapping>
        { "useGlobalIdCounter" : true }
      </mapping>
      <expect><![CDATA[
      @imports
      import net.democritus.sys.IdCounterLocal;
      @/imports

      @variables
      @EJB private IdCounterLocal idCounterLocal;
      @/variables
      ]]></expect>
    </temlateTest>
  </templateTests>
</featureTest>

Expanded tests as Java:

package net.democritus.expander.ejb3.dataElement;

// expanded with FeatureTestExpander version X.Y.Z

public class BeanExpanderGlobalCounterTest {
  final ModelScenarioBuilder builder = new ModelScenarioBuilder();
  private ComponentExpansionContext ComponentExpansionContext;
  private ApplicationInstanceComposite applicationInstance;
  private DataElementComposite dataElement;

  @Before
  public void setUp() {
    // to avoid pointless duplication in tests
    componentExpansionContext = new ExpansionContextTestScenario().componentExpansionContext();
    builder.setComponent(componentExpansionContext.getComponentComposite());
    applicationInstance = componentExpansionContext.getApplicationInstanceExpansionContext().getApplicationInstanceComposite();

    setUpBaseModel();
  }

  public void setUpBaseModel() {
    // <baseModel>
    //   dataElement X {
    //   }
    // </baseModel>
    dataElement = builder.addDataElement("X").getValue();
  }

  public void test_mapping_1() {
    // <mappingTest>
    //   <given>
    //     globalOptionSettings {
    //       counterDefault = "IDENTITY"
    //     }
    //   </given>
    //   <expect>
    //     { "useGlobalIdCounter" : false }
    //   </expect>
    // </mappingTest>

    // given
    applicationInstance.getGlobalOptionSettings().setCounterDefault("IDENTITY");

    // expect
    Map<String, Object> expected = new HashMap<String, Object>();
    expected.put("useGlobalIdCounter", false);

    // perform
    Map<String, Object> actual = expander(dataElement).getModel();
    assertMapContains(expected, actual);
  }

  public void test_template_1() {
    // <templateTest>
    //   <mapping>
    //     { "useGlobalIdCounter" : true }
    //   </mapping>
    //   <expect><![CDATA[
    //   @imports
    //   import net.democritus.sys.IdCounterLocal;
    //   @/imports
    //
    //   @variables
    //   @EJB private IdCounterLocal idCounterLocal;
    //   @/variables
    //   ]]></expect>
    // </temlateTest>

    // mapping
    Map<String, Object> mapping = new HashMap<String, Object>();
    mapping.put("useGlobalIdCounter", true);

    // perform
    ArtifactExpander expander = expander(dataElementPerson);
    Map<String, Object> model = expander.getModel();
    model.putAll(mapping);

    String result = getResult(model);

    // expect
    assertThat(result, containsInAnchor("imports",
      "import net.democritus.sys.IdCounterLocal;"));
    assertThat(result, containsInAnchor("variables",
      "@EJB private IdCounterLocal idCounterLocal;"));
  }

  // helper methods, getResult(), Feature & FeatureTest XML loading, etc.
}

results matching ""

    No results matching ""