seamer
seamer copied to clipboard
refactoring tool that aims at making it easy to create characterization tests
Seamer - gets your legacy code under test
Seamer aims at making it easy for you to create characterization tests.
Usage
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.
Taking advantage of closures
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.
Recording some invocations
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();
Verifying the Seam in a test-harness
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.
Verifying the results using Object equality
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verify();
}
Verifying the results comparing field by field
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verifyComparingFields();
}
Verifying the results comparing the toString representations
@Test
void verify_everything_still_works() {
Seamer.create().get("MySeam").verifyComparingToString();
}
Verifying the results providing a custom comparator
@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.
Persistence Configuration
The default
By default Seamer is using a file-based persistence that stores its data into src/test/java/seamer
Custom Path
If you wanted to store Seamers recorded data somewhere else, just define the path like so
Seamer.create("/tmp/seamer")
Sqlite
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())
Create a seam via Proxy
Spring+AspectJ
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();
}
}
cglib
SeamerCglibFactory.createProxySeam(ClassCaputringTheSeam.class, "legacyMethod", "MySeam")
Limitations
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
Suture
This tool is inspired by https://github.com/testdouble/suture which does a similar thing in ruby.