GDPR Cookie Consent by Free Privacy Policy Generator
post thumb
Test Automation
by Matthias Hanitzsch-Moonlight/ on 02 Apr 2018

Cucumber - Timeseries Validation Functions

In this article I show how to implement a Cucumber test for timeseries validation.

The feature file

I will use the non-positive check as an example. This timeseries validation is a standard Asset Control function that will flag any numerical value equal to or less than zero as suspect. Well, that is what the manual says. I want to see for myself.

I create another feature file, e.g. src/test/features/tsvalidation/ts_validation_negative.feature with the following content:

Feature: The timeseries validation non-positive flags values equal to or less than zero

  Scenario: A positive value is not flagged
    Given an ADO with the following timeseries in CONSOLIDATION_C0
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | 100 [1] |
    When we apply the validation function non-positive and revalidate
    Then we get the following timeseries
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | 100 [1] |

  Scenario: A zero value is flagged
    Given an ADO with the following timeseries in CONSOLIDATION_C0
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | 0 [1] |
    When we apply the validation function non-positive and revalidate
    Then we get the following timeseries
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | 0 [130] |

  Scenario: A negative value is flagged
    Given an ADO with the following timeseries in CONSOLIDATION_C0
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | -100 [1] |
    When we apply the validation function non-positive and revalidate
    Then we get the following timeseries
      | DATE     | TIME   | CLOSE   |
      | 20180702 | 235959 | -100 [130] |

There are three scenarios: Values greater than zero should not get flagged. Values equal to zero and values less than zero should get flagged.

I use data tables to represent timeseries, with the attributes as the header. To denote the status of a value, I append that wrapped in square brackets.

You also see that the ADO ID or even its prefix is not important and hence is not mentioned in the feature file.

Implementing the steps

To hold the implementation of the steps above, I create a Java class in src/test/java/io/terrafino/cucumber/steps/TsValidationTestSteps.java.

I will present this in multiple parts:

Foundational work

package io.terrafino.cucumber.steps;

import cucumber.api.DataTable;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import io.terrafino.api.ac.AcException;
import io.terrafino.api.ac.ado.Ado;
import io.terrafino.api.ac.attribute.Attributes;
import io.terrafino.api.ac.service.AcConnection;
import io.terrafino.api.ac.service.AcService;
import io.terrafino.api.ac.timeseries.TsRecord;
import io.terrafino.api.ac.timeseries.TsRecords;
import io.terrafino.api.ac.validation.TsValidation;
import io.terrafino.api.ac.validation.TsValidationNonPositive;
import io.terrafino.api.ac.value.Value;
import io.terrafino.api.ac.value.ValueFactory;
import io.terrafino.api.ac.value.Values;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

public class TsValidationTestSteps {

    private static AcConnection conn;
    private static AcService ac;
    
    private Ado ado;
    private String tree;

    @Before
    public void before() throws AcException {
        if (conn == null) {
            conn = AcConnection.getDefaultConnection();
            ac = new AcService(conn);
        }
    }

    @After
    public void after() throws AcException {
        if (Optional.ofNullable(ado).isPresent()) {
            ado.delete();
            ado.deleteTimeseries(tree);
            ado = null;
        }
    }

    // see below
}

As before, we have the AcConnection and the AcService. The before method takes care of connecting to AC and initialising the AcService. The after method deletes the test ADO, including timeseries!

Given, When, Then

Now, for the implementationn of Given/When/Then:


    @Given("^an ADO with the following timeseries in (\\S+)$")
    public void anAdoWithTimeseries(String tree, DataTable datatable) throws AcException {
        this.tree = tree;
        ado = ac.testAdoWithPrefix("C0.TEST")
                .withTemplate("C0_I_T001_LSA")
                .createInAc();
        ado.storeTimeseries(tree, getTimeseriesFrom(datatable));
    }

    @When("^we apply the validation function (\\S+) and revalidate$")
    public void weSetAttrToValue(String valFunc) throws AcException {
        ado.addTimeseriesValidation(tree, getValidationFunction(valFunc));
        ado.revalidate(tree);
    }

    private TsValidation getValidationFunction(String valFunc) {
        switch (valFunc) {
            case "non-positive": return new TsValidationNonPositive();
            default: throw new IllegalArgumentException(String.format("Unknown validation function: %s", valFunc));
        }
    }

    @Then("^we get the following timeseries$")
    public void weSetAttrToValue(DataTable datatable) throws AcException {
        TsRecords expectedRecords = getTimeseriesFrom(datatable);
        TsRecords acRecords = ado.loadTimeseries(tree, 0, 0, expectedRecords.getAttributes());
        assertThat(acRecords.equalsWithStatus(expectedRecords), is(true));
    }

A quick breakdown:

  • The @Given method creates a test ADO and stores the timeseries from the data table in the given tree.
  • The method getTimeseriesFrom is discussed below.
  • The @When method applies the specified validation function and revalidates the timeseries.
  • The method getValidationFunction will need to be extended to support other validation functions going forward.
  • The @Then method reads the actual timeseries from the ADO and ensures it matches the timeseries as provided in the data table.
  • Note the use of equalsWithStatus in the assert to ensure not only the values are the same, but also their status.

The last missing piece is the function that transforms the timeseries data table into actual timeseries records:

From data table to timeseries records

    private TsRecords getTimeseriesFrom(DataTable datatable) {
        List<List<String>> rows = datatable.asLists(String.class);
        List<String> header = rows.get(0);
        Attributes attributes = new Attributes(
                header.stream().filter(a -> !(a.equals(DATE) || a.equals(TIME))).collect(Collectors.toList())
        );
        List<TsRecord> records = new ArrayList<>();
        for (int i = 1; i < rows.size(); i++) {
            int date = Integer.parseInt(rows.get(i).get(header.indexOf(DATE)));
            int time = Integer.parseInt(rows.get(i).get(header.indexOf(TIME)));
            List<Value> values = new ArrayList<>();
            for (String a : attributes.getAll()) {
                values.add(ValueFactory.createValueFromString(rows.get(i).get(header.indexOf(a))));
            }
            records.add(new TsRecord(date, time, attributes, new Values(values)));
        }
        return new TsRecords(records);
    }

Here is what happens:

  • I transform the data table into a list of lists of strings (line 2).
  • I extract the header (line 3).
  • From the header, I create the list of attributes, excluding DATE and TIME (line 4-6).
  • In lines 8-16, I loop over the remaining rows in the data table.
  • I extract date and time (using the index of DATE and TIME in the header rather than making any assumption of their position, lines 9-10).
  • For the remaining attributes, I create a list of values based on their string representation (lines 11-14).
  • The method ValueFactory.createValueFromString has the necessary logic to identify both value and status.
  • In line 15, I keep track of the extracted records.
  • And finally, in line 17, I can return the final TsRecords object.

Running the test

Please refer to the instructions here.

Thank you for reading!

comments powered by Disqus