Skip to main content

Java SDK

Java SDK reference to build your own generators.

Define a supplier and a generator

The minimum setup is to have one supplier with one generator. Those can be defined as follows:


@SupplierName("Billing")
public class Supplier2 extends Supplier {
public static void main(String... args) {
new Supplier2()
.withGenerators(
new InvoiceGenerator(),
new LoanGenerator())
.bootstrap();
}
}

@ItemName("Invoice")
public class InvoiceGenerator extends Generator {

public record InvoiceRequest(Language language,
Integer amountToBill,
Boolean extendedTerm,
String vatId) {
}

public enum Language {English, French}

public record Invoice(String invoiceId) {
}

public Invoice generate(InvoiceRequest input) {
var randomId = String.valueOf(System.currentTimeMillis() % 1000);
return new Invoice(randomId);
}

}

It is required that the supplier extends Supplier and that the generator extends Generator.

The annotation @SupplierName is mandatory and uniquely identifies the supplier on a given environment. It is case-sensitive.

The name of the item (in the example above it's Invoice) uniquely identifies the item in scope of a given supplier on a given environment.

Bootstrap the supplier

To bootstrap the supplier and all declared generators, you can use in plain Java:

public class WhateverClass {
public static void main(String... args) {
new BillingSupplier().withGenerators(new InvoiceGenerator()).bootstrap();
}
}

or in Spring:


@Component
public class WhateverClass {

@PostConstruct
public void init() {
new BillingSupplier().withGenerators(new InvoiceGenerator()).bootstrap();
}
}

Sixpack SDK launches a separate set of threads to operate and handles failures with a back-off strategy. It will cause application restart in following conditions:

  • Cannot connect to the Sixpack platform at startup (later losing the connection will only trigger reconnection with a back-off strategy)
  • Some other critical error occurs (out of memory or similar)

Detailed description of a generator

Generator inputs and outputs

Any generator input is an object with any number of fields that must be of type String, Integer, Long, Short, Boolean, enum. The inputs define how will look like the specification form on the UI and what inputs will be accepted on the REST interface. Types are mapped as follows:

  • String - text input with possible null value
  • Integer, Long, Short - number input with possible null value
  • Boolean - 3-state checkbox (true, false, null)
  • enum - dropdown with possible null value

Any generator output is a similar object. In more complex cases an output of one generator can be used as input of another generator. That is why the field types must follow the same rules.

The objects used as input or output can be of any type including Java records with following limitations:

  • They must be serializable by Jackson (and you may use related annotations).
  • They must have a flat structure (no nested objects).
  • Only allowed field types, see above.

Method generate

The method generate is mandatory. It performs the required operation to produce a dataset and returns necessary information, usually just a subset of the dataset. It should handle properly null fields in the input by e.g. generating random values as per required business logic. It must:

  • Not override generate from Generator
  • Be public
  • Have a first argument representing the input
  • Return an object representing the output
  • Complete within 30s or call heartbeat() at least every 30s It may have:
  • A second argument of type Context see below for more details.

Iterative generation

Instead of directly returning the output object, the method generate can end by calling haltAndIterateAfter(Duration) or haltAndIterateAfter(duration, message). In that case the generate method is halted at that point and will be restarted from the beginning after the specified timeout. Along with the initial input a second parameter of the method generate will hold any arbitrary contextual data that has been stored by the previous iteration. The optional message passed to haltAndIterateAfter will be displayed on the UI as a progress message.

This allows to implement simple asynchronous scenarios such as polling some system for an expected result before continuing with the test dataset construction. Along with the user specified contextual data, Sixpack provides some built-in information such as the index of the iteration.

Example in which:

  • The first iteration does some initial setup
  • Next iterations check for some condition every 5 minutes and only then return the output
@ItemName("Loan")
public class WhateverClass {
public Loan generate(LoanRequest loanRequest, Context context) {
if (context.getIteration() == 0) {
// ...
context.set("someKey", myVariable);
haltAndIterateAfter(Duration.ofMinutes(10));
}
var myVariable = context.get("someKey");
// ...
if (dataNotYetReady) {
haltAndIterateAfter(Duration.ofMinutes(5));
}
return new Loan(...);
}
}

Note: In more complex scenarios involving several generators a better option is available with orchestrators which or generators that are able to request and consume data from other generators.

Method configure

It can be used to configure a prebuilt dataset just before it is allocated to the user. In cases when creating a dataset takes time but adding some label e.g. tester's name is a fast operation, that operation can be done in this configure method only once it is clear which tester will get that dataset.

This method is optional. If not defined, not configuration will be done.

Method cleanup

This method is optionally called after a defined period of time when the dataset was allocated to a tester. The time can be defined in the annotation @Lifespan. By the default the lifespan is 30 days. It allows to perform any cleanup opeations e.g. delete the dataset from the test environment.

This method is optional. If not defined, no cleanup will be done.

Method templates

This method is optionally used to define a list of templates and their minimum count to be always maintained in stock. When some datasets take time to produce and / or their production is not stable, specified datasets returned by this method will be requested upfront so that they are already available when a tester requests them. The templates define the input combinations. The structure to return is a list of templates where a template is a pair of input and minimum count.

For example:

public class InvoiceGenerator extends Generator {

(...)

public List<Template> templates() {
return List.of(
new Template(
new InvoiceRequest(Language.English, null, null, null),
10),
new Template(
new InvoiceRequest(Language.French, null, null, null),
5));
}
}

This will tell Sixpack to always try to maintain at least 10 invoices in English and 5 invoices in French in stock.

In case the method templates is not defined, Sixpack will compute templates automatically by creating all combinations of fields of type enum and Boolean and will try to maintain at least 1 for each such combination. If you would like not to pre-generate datasets, just return an empty list from templates.

Orchestrator

To be able to combine or chain the output of generators, it is possible to use an orchestrator. It behaves almost the same as a generator with following differences:

  • Must be deterministic
  • Can use the method obtain to request data from other generators

Bootstrap with an orchestrator


@SupplierName("Billing")
public class BillingSupplier extends Supplier {
public static void main(String... args) {
new BillingSupplier()
.withOrchestrators(
new InvoiceOrchestrator())
.bootstrap();
}
}

public class InvoiceOrchestrator extends Orchestrator {

public record InvoiceRequest(Language language,
Integer amountToBill,
Boolean extendedTerm,
String vatId) {
}

public enum Language {English, French}

public record CustomerRequest(Language language){}

public record Customer(String customerId) {}

public record Invoice(String customerId,
String invoiceId) {}

public Invoice generate(InvoiceRequest input) {
var customer = obtain(new CustomerRequest(input.language()));
var randomId = String.valueOf(System.currentTimeMillis() % 1000);
return new Invoice(customer.customerId(),randomId);
}

}

The method obtain behaves similarly to dataset request on the UI or via REST so in case the requested dataset is in stock, it's returned immediately, otherwise it is requested ad-hoc to the corresponding generator or orchestrator. Yes orchestrators can obtain data from other orchestrators making it very easy to compose complex datasets using simple native code.

Determinism

Determinism is important for technical reasons and is a downside of allowing to have plain code representing your business logic in the orchestrator. Being deterministic means that it:

  • Must not access external systems directly, only through generators with the obtain method
    • This includes accessing a database or local file system
  • Must not use time and random functions directly but only through provided helper methods

Following happens under the hood:

  • Each method call of the method obtain() anywhere in the code causes the code to be replayed from the beginning once the obtain method returns.
  • During the replay phase, methods obtain() and any of the provided deterministic methods are skipped with previous results injected.
  • In case you use a non-deterministic method such as Instant.now(), it will be called again with a different result.

The impact is anything between:

  • No impact: your code uses the value of the non-deterministic method only once
  • Full failure: some if switch decides based on the output of your non-deterministic method and will cause the replay phase to fail because the history of calls does not match current order of calls --> the whole orchestrator task will be abandoned and restarted a few times before a final failure.

Helper methods for time and random functions

The SDK provides equivalents of main time and random functions so that they can be used in the orchestrator in the class Deterministic. For example

  • deterministicNow() instead of Instant.now()
  • deterministicZonedNow(ZoneId zoneId)
  • deterministicRandomInt(int lowerBound, int upperBound)
  • ...