GIWT is a java test library based on JUnit platform. It gives developers the ability to write unit tests in the GWT (Given-When-Then) format.
This is a simple usage example π
import io.github.imagineDevit.giwt.annotations.Test;
class MyTest {
@Test("1 + 1 should be 2")
void test(TestCase<Integer, Integer> testCase) {
testCase
.given("state is 1", 1)
.when("1 is added to the state", i -> i + 1)
.then("the result should be equal to 2", result -> result.shouldBe().equalTo(2));
}
}
As seen in the example above, the test method takes a TestCase<T,R>
as a parameter.
TestCase<T,R>
.
TestCase is a generic Object that takes two types as parameters : T
and R
.
T
represent the type of the state of the test. It can be any type. Use Void
if you don't need a state.
R
represent the type of the result of the test. It can be any type. Use Void
if you don't need a result.
TestCase
and its linked statements (GivenStmt<T,R>, WhenStmt<T,R> and ThenStmt<T,R> ) come with a set of methods that can be chained to write the test in the GWT format.
Each method takes a string first parameter representing the statement description.
-
This method sets the initial state of the test. It returns a GivenStmt<T,R> object.
-
This method represents the action to be tested. It returns a WhenStmt<T,R> object.
-
This method allows the verification of the result of the test. It returns a ThenStmt<T,R> object.
-
GivenStmt<T,R>
andThenStmt<T,R>
classes have anand()
method that allows to chain multiple statements of the same type.
When it comes to testing a method, the test state is represented by the method parameters.
Thus, can we represent the state of a method test as an object of type T
when the method has a single parameter
otherwise as a List<Object>
.
Managing a List<Object>
as test state is possible but may not be very conformable.
This is why GIWT introduces @GiwtProxyable annotation.
@GiwtProxyable annotation has a processor that generate a proxy class
for each annotated class and a parameters record
for each public method with more than one parameter.
This parameters record
contains all parameters of the given method.
The proxy class
has a one parameter proxy methods
for each original class public method.
Let's see an example
Consider the following class π
package io.example.helpers;
import io.github.imagineDevit.giwt.annotations.GiwtProxyable;
@GiwtProxyable
public class StringHelper {
public String repeat(String text, int times, String separator) {
return String.repeat(separator + times).replaceFirst(separator, "");
}
}
After compiling your project, the following proxy class should be generated π:
package io.example.helpers;
public class StringHelperTestProxy {
private final StringHelper delegate;
public StringUtilsTestProxy(StringHelper delegate) {
this.delegate = delegate;
}
record RepeatParams(String text, int times, String separator) {}
public String repeat(RepeatParams param) {
return this.delegate.repeat(param.text(), param.times(), param.separator());
}
}
As seen above, the generated record name is the name of the method suffixed with Params.
What if we have method overloading (several methods with the same name and different parameters) in our class?
π§¨οΈ Compilation will fail !!!!οΈ
To solve the problem, annotate the overloaded method with @ParameterRecordName and specify the name of the record.
import io.github.imagineDevit.giwt.annotations.ParameterRecordName;
@ParameterRecordName("justRepeat")
public String repeat(String text, int times) {
return String.repeat(times);
}
The generated proxy class
can then be used for testing as follows:
import io.github.imagineDevit.giwt.TestCase;
import io.github.imagineDevit.giwt.annotations.Test;
import static io.example.helpers.StringHelperTestProxy.*;
class StringHelperTest {
private StringHelperTestProxy proxy = new StringHelperTestProxy(new StringHelper());
@Test
public void repeat(TestCase<RepeatParams, String> testCase) {
testCase
.given("a param", new RepeatParams("A", 3, "_"))
.when("repeat is called", proxy::repeat)
.then("the result should be A_A_A", result -> result.shouldBe().equalTo("A_A_A"));
}
@Test("just repeat A 3 times should give AAA")
public void justRepeat(TestCase<JustRepeatParams, String> testCase) {
testCase
.given("a param", new JustRepeatParams("A", 3))
.when("repeat is called", proxy::repeat)
.then("the result should be AAA", result -> result.shouldBe().equalTo("AAA"));
}
}
In some cases, it is necessary to store variables other than the state and the result of the test. In this case a testCase can be converted into a TestCaseWithContext<T,R>
by calling the withContext()
method.
TestCaseWithContext<T,R>
offers some methods :
- to store state :
setState(T state)
- to transform state:
mapState(Function<T,T> mapper)
- to map state into result :
stateToResult(Function<T,R> mapper)
- to store a context variable :
setVar(String key, Object value)
- to get a context variable :
getVar(String key)
This is a simple usage example π
class MyTest {
@Test("1 + 1 should be 2")
void test(TestCase<Integer, Integer> testCase) {
testCase.withContext()
.given("the state is set to 1", ctx -> ctx.setState(1))
.when("result is set to state + 1", ctx -> ctx.stateToResult(state -> state + 1))
.then("the result should be 2", (ctx, result) ->
result.shouldBe()
.notNull()
.equalTo(2)
);
}
}
GIWT provides a set of annotations that can be used to configure the test classes and methods.
-
GIWT provide a custom annotation
@Test
that can take a string as a parameter representing the test name.If no parameter is provided, the test name will be the method name.
-
This annotation marks a test method as parameterized. It can take two parameters :
-
name
: the test name. If not provided, the test name will be the method name. -
source
: the parameter source. Parameter source can be :- a method annotated @ParameterSource that returns a
TestParameters
object. (The source is the@ParameterSource
value)) - a entry of the Map<String, TestParameters> returned by the test configuration class (The source is the entry key)
- a method annotated @ParameterSource that returns a
A parameterized test method must have all paremeters as arguments. The order of the parameters must be the same as the order of the parameters in the
TestParameters
object. -
class MyTest {
@ParameterizedTest(
name = "Length of {0} should be equal to {1}",
source = "getParams"
)
void test2(TestCase<String, Integer> testCase, String text, Integer expectedResult) {
testCase
.given("state is %s".formatted(text), () -> text)
.when("state length is evaluated", i -> i.length())
.then("the result should be %d".formatted(expectedResult), result ->
result
.shouldBe()
.equalTo(expectedResult)
);
}
@ParameterSource("getParams")
private TestParameters<TestParameters.Parameter.P2<Integer, Integer>> getParams() {
return TestParameters.of(
TestParameters.Parameter.P2.of("Hello", 5),
TestParameters.Parameter.P2.of("Bonjour", 7),
TestParameters.Parameter.P2.of("Good morning", 12)
);
}
}
Length of {0} should be equal to {1}
-
This annotation marks a test method as skipped.
-
This annotation marks a method as a parameter source for a parameterized test method. It can take a string as a parameter representing the parameter source name. If no parameter is provided, the parameter source name will be the method name.
-
This annotation marks a method as a before each method. It will be executed before each test method.
-
This annotation marks a method as a after each method. It will be executed after each test method.
-
This annotation marks a method as a before all method. It will be executed before all test methods.
-
This annotation marks a method as a after all method. It will be executed after all test methods.
-
This annotation registers a list of callbacks for a test class. It can take a list of classes as a parameter. These classes must implement the BeforeEachCallback, BeforeAllCallback,
AfterEachCallback and/or AfterAllCallback interfaces. -
This annotation registers a class as the test class configuration. It take a class that must implement TestConfiguration as parameter
GIWT provides a report generation feature. This feature is disabled by default.
To enable it, you must add a new environment variable giwt.generate.report = true
.
The report file report.html
is generated and stored in the target/giwt
directory.