Seamer aims at making it easy for you to create characterization tests.
Suppose you have a code like the following:
String result = someReallyComplicatedLegacyMethod(String param1, Integer param2);
doSometingWith(result);
You have no idea what someReallyComplicatedLegacyMethod
is doing so you want to refactor it safely.
This is where Seamer comes in handy.
Seamer allows you to intercept the method and record all invocations and results like the following:
String result = Seamer.create()
.define("MySeam", // this is just an id of your choice
(InvokableWith2Arguments<String, Integer, String>) this::someReallyComplicatedLegacyMethod)
.invokeAndRecord(param1, param2);
doSometingWith(result);
Now the code still does the same thing, plus it will record all invocations of the Seam.
As the lambda expression results in a closure that captures its surrounding state,
the recordings of this seam will be repeatable even if they depend on surrounding state.
Suppose the method is incrementing an int
field of the surrounding class,
that side effect will be repeated when we verify the seam.
Check out the ClosureSeamTest which demonstrates this behavior.
You may now run your application. You would click around the UI and have this thing invoked a couple times with realistic arguments. Or, you would record some invocations using the following code.
// shuffles given argument candidates, records all possible scenarios and its results.
Seamer.create()
.customRecordings("MySeam")
.addArgCandidates(0, "hello", "world", null)
.addArgCandidates(1, () -> asList(1, 2, 3))
.shuffleArgsAndExecute();
You may now setup a test that replays all recorded invocations and verifies if the code is still doing what it is supposed to do.
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verify();
}
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verifyComparingFields();
}
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verifyComparingToString();
}
@Test
void verify_everything_still_works() {
// lambda signature:
// BiConsumer<ObjectAssert<T>, Object> verification
Seamer.create().get("MySeam").verify(AbstractAssert::isEqualTo);
}
And that's it. you may now refactor the code using this test as a feedback tool.
By default Seamer is using a file-based persistence that stores its data into src/test/java/seamer
If you wanted to store Seamers recorded data somewhere else, just define the path like so
Seamer.create("/tmp/seamer")
If you wanted to store Seamers recorded data in an Sqlite DB, create Seamer like the following
Seamer.create(new SqlitePersistence("jdbc:sqlite:/tmp/seamer"))
or even shorter:
Seamer.create(SqlitePersistence.atTmp())
public static class AspectJDemo {
@Seam("MySeam")
public String legacyMethod(String arg1, Integer arg2) {
return arg1 + arg2;
}
}
Make sure to enable the SeamerAspect
@EnableAspectJAutoProxy
@Configuration
class SeamerConfig {
@Bean
public SeamerAspect seamerAspect() {
return new SeamerAspect();
}
}
SeamerCglibFactory.createProxySeam(ClassCaputringTheSeam.class, "legacyMethod", "MySeam")
Seamer needs objects to have at least private no-arg constructors for deserialization.
final
fields that are initialized in another no-arg constructor won't be able to be properly initialized.
TBD, allow users to register ObjectInstantiators
This tool is inspired by https://github.com/testdouble/suture which does a similar thing in ruby.