Rhino Load and Performance Testing framework is celebrating its 1.8.0 release, with the new version, the Rhino Load DSL (beta) reached another milestone towards its production readiness as it enables developers to write load and performance tests with its new DSL operations. Load DSL, in addition to Scenarios, allows developers to write their tests in a declarative style. DSL is a powerful tool to tell Rhino how synthetic load has to be generated, instead of providing the actual load generation implementation; hence, the load testing code doesn’t need to manage the concurrency and/or client configurations if your tests, for instance, are targeting web services. The framework will then materialize the DSL into reactive components and the concurrency will be managed by the reactive machinery under the hood.
Since the Scenario mode in Rhino simulations is the default one, if you want to enable the DSL mode, you need to change the Runner implementation by adding the @Runner annotation to your Simulation class and passing the ReactiveHttpSimulationRunner to its clazz attribute:
@Simulation(name = "File Upload Simulation") ❶
@Runner(clazz = ReactiveHttpSimulationRunner.class) ❷
@UserRepository(factory = OAuthUserRepositoryFactoryImpl.class)
public class UploadDSL {
@Provider(factory = UUIDProvider.class)
private UUIDProvider uuidProvider;
@Dsl(name = "Upload File") ❸
public LoadDsl singleTestDsl() {
return Start.dsl() ❹
.run(http("PUT text.txt") ❺
.header(c -> from(X_REQUEST_ID, "Rhino-" + uuidProvider.take()))
.header(X_API_KEY, SimulationConfig.getApiKey())
.auth()
.endpoint((c) -> "http://foo/" + uuidProvider.take())
.upload(() -> file("classpath:///test.txt"))
.put()
.saveTo("result")); ❻
}
}
Simulation entity starts with the @Simulation ❶ annotation which marks the class as a load testing
entity. To enable the DSL mode, we use @Runner ❷ with the simulation runner implementation, ReactiveHttpSimulationRunner (Default runner is the DefaultSimulationRunner which runs the Scenario methods). DSL methods are marked with @Dsl ❸ with a name which is used in reporting of performance measurements, that is mandatory. The implementation of the DSL method uses chained-style method invocations, starts with Start.dsl()
❹ and followed by runner methods, like run, runIf, runUntil, etc. ❺ Runner methods takes Specs instances as parameters. A Spec describes how a load test action to be performed. The result of execution will be stored in the
session with ❻ for the next runners and specs.
You can now run the “File Upload Simulation” with a simple Java application:
public static void main(String ... args) {
Simulation.create(PROPS, RhinoDSL.class).start();
}
The framework will then generate the load according to the DSL.
Load DSL is extensible
Rhino DSL framework is extensible. In addition to the specs which framework provides for you, by the way, you can easily add new spec types and materializers thereof to extend the DSL framework. A spec materializer is a component which takes spec instances as input and creates reactive components.
Simulations might have one or more Dsl methods and each DSL method comprises a set of runner methods. Runner methods takes the Spec instances as parameters and materializes them by using spec materializers.
Let’s have a look at an example spec, SomeSpec which is the spec for executing arbitrary code:
public interface SomeSpec extends Spec {
Spec as(Function<UserSession, String> function);
Function<UserSession, String> getFunction();
}
as()
method takes an argument of Function<UserSession, String> which is run by the enclosing runner. The Spec implementation is just a simple builder:
public class SomeSpecImpl extends AbstractSpec implements SomeSpec {
private Function<UserSession, String> function;
public SomeSpecImpl(final String measurement) {
super(Objects.requireNonNull(measurement));
}
@Override
public Spec as(final Function<UserSession, String> function) {
this.function = Objects.requireNonNull(function);
return this;
}
@Override
public Function<UserSession, String> getFunction() {
return function;
}
}
as Spec instances are holding the information which is needed in materialization process, materializers take the information conveyed by specs and transform them into reactive components. A simple materializer which takes the Spec instance and produces a Mono:
public class SomeSpecMaterializer implements SpecMaterializer<SomeSpec, UserSession> {
@Override
public Mono<UserSession> materialize(SomeSpec spec, UserSession userSession) {
return Mono.just(userSession)
.flatMap(session -> Mono.fromCallable(() -> {
var status = spec.getFunction().apply(session);
return session;
}));
}
}
I hid some details about performance measurement in the implementation to keep the example simple. If you want to throw a look into the full implementation of SomeSpecMaterializer, please follow the link.
Now, you can use your new spec in the DSL:
@Dsl(name = "Some")
public LoadDsl singleTestDsl() {
return Start.dsl()
.run(some("test") ❶
.as(session -> { ❷
session.add("say", "hello world");
return "OK";
}))
.saveTo("result");
}
}
some()
method ❶ is just a static factory which creates a new SomeSpecImpl intance for convenience. SomeSpec function takes a session object ❷ as parameter. Sessions are contextual objects to store data. For more information about sessions, please follow the link. That’s it. You have just created your first functional Spec component.
Wrap-up
Load DSL is the domain specific language to implement load and performance tests in Rhino. DSL declares how synthetic load is to be generated whereas the scenario methods contain actual implementation of the load generation process. Load DSL is extensible. You can add your own specs and materializers to create new operations. Furthermore, using Load DSL comes at a price. If your tests are getting more complex, your DSL methods will get longer and readability may suffer from long chained method calls. So, you might prefer to use the Scenario mode, instead.