He was here a minute ago...
...replacing Mockito, byte-buddy, and a complicated 3rd party dependency closure with software.visionary.carlin.
George Carlin said a house is just a place to keep your stuff while you go out and get more stuff. A dependency closure is the same thing for jars.
Mine held Mockito. Mockito brought Byte Buddy, Byte Buddy's agent and
Objenesis, and Byte Buddy brought its own copy of ASM. That's 5.68 MB of runtime bytecode generation,
two Java agents strapped to every test fork bob launched, and two whole
Kompuwiz modules, mockito-closure and
byte-buddy-closure, whose only job was to name that pile of jars
once so nobody had to look at it. All of it, so a test could say "pretend this
interface returns 3."
Now look for it.
No test in the fleet imports org.mockito. None imports
net.bytebuddy or org.objenesis. Both closures are
deleted. In their place is
carlin: 774 lines on
the JDK alone, in a 38 KB jar. 512 test classes in 195 modules use it, and for
134 of those modules the move changed nothing but import lines.
He was here a minute ago.
The anti-corruption layer
When I first wired Mockito into
spex, I didn't want my
tests talking to it directly. Its surface is enormous, its static methods put
org.mockito types into every test that touches them, and its
closure is the kind of thing that moves into your module graph and never
moves out. So I did what Eric Evans tells you to do with a model you don't
want leaking into yours: I built an ANTI-CORRUPTION LAYER.
One FACADE, software.visionary.spex.mocking.Doubles, with the
handful of verbs a specification needs (mock,
stub, confirm, the matchers, a few frequencies), and
a javadoc that said exactly what it was for:
/**
* Main entry point for test doubles, encapsulating Mockito so downstream code
* never imports {@code org.mockito} directly.
*/
That one decision is the whole story. Everything below is what it bought.
A conversation with Gemini
On 2026-09-08 I pasted that facade into Gemini and asked:
How could I use the ClassFile API to build a replacement for Mockito and all its byte-buddy/asm coupled garbage given the following ANTI-CORRUPTION LAYER?
It opened by flattering me, the way they do:
Because you had the foresight to isolate Mockito behind a rigid Anti-Corruption Layer (ACL) and enforce a strict Facade pattern, you are in a uniquely powerful position. You can completely rip out Mockito, Byte-Buddy, ASM, and Objenesis from your dependency tree, and zero downstream tests will need to change.
Then it drew me four pillars: a ThreadLocal state machine, proxy
classes generated with java.lang.classfile and loaded as hidden
classes, sun.reflect.ReflectionFactory to skip constructors the
way Objenesis does, and a ClassFileTransformer agent to rewrite
static methods the way mockito-inline does. Rebuild Mockito, minus Mockito.
I pushed on the first pillar, because global mutable state in a test framework is how you get tests that pass alone and fail together:
Do I really need the ThreadLocal state machine? Aren't those just lambda DECORATORs I can generate internally? When do I need global invocation state?
That's where it earned its keep. Java evaluates arguments inside out, so
stub(mock.process(any())) is three separate events, and by the
time stub runs, all it's holding is the null the
double returned:
You created the Doubles Facade to protect your business logic from Mockito's bytecode generation, which it does perfectly. But by mirroring Mockito's exact syntax, you accidentally inherited Mockito's temporal coupling.
The whole conversation is worth a read. It was also a map to the wrong place. Two of those four pillars, the constructor bypass and the agent, are precisely what makes Mockito heavy.
The spike
Three weeks later I asked a Claude agent for a design pitch, and it measured before it pitched. 2,027 places in the fleet's tests made a double; 95% of them doubled an interface or an annotation. Then it wrote a spike against the facade's names and ran the fleet's own tests on it:
648 lines. The same verdict as Mockito on 94% of the tests it could run, on the first try.
What carlin is
carlin keeps the facade's names, so a test moves by changing what it imports. Take tls, my self-hosted TLS 1.3: 784 tests over the handshake state machine, client certificates, peer chains and admission, with doubles standing in for certificates all through them. This is its entire migration, repeated in eight files:
import software.visionary.spex.Fact;
-import software.visionary.spex.mocking.Doubles;
+import software.visionary.carlin.Doubles;
import software.visionary.tls.Alert;
...
private static final Certificate LEAF = Doubles.mock(Certificate.class);
private static final Certificate ISSUER = Doubles.mock(Certificate.class);
private static final Certificate STRANGER = Doubles.mock(Certificate.class);
Eight lines, all imports. 784 tests green. And the agent that made the change wasn't my heavy hitter; it was a Claude Sonnet. That's not one lucky module: 134 modules moved in commits that touched nothing but imports, 647 lines of them, and Sonnet made 103 of those commits. Better Tools make Simpler Intelligences able to Do More, and the best tool here was a facade I wrote before any of this was a plan.
Underneath, the bytecode circus has left town. A double of an interface is a
java.lang.reflect.Proxy, in the JDK since 1.3. A double of a
class made to be extended is a hidden subclass, written with the ClassFile API
that went standard in JDK 24:
/** @return what stands in for an interface, a proxy, or for a class, a subclass of it */
private <T> T standing(final InvocationHandler handling) {
return (T) (type.isInterface()
? Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handling)
: Subclassed.of(type, handling));
}
final byte[] standing = ClassFile.of().build(STANDING, written -> {
written.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_FINAL | ClassFile.ACC_SYNTHETIC);
written.withSuperclass(described(type));
written.withInterfaceSymbols(described(Held.class));
written.withField(KEPT, HANDLING, ClassFile.ACC_PRIVATE | ClassFile.ACC_FINAL);
// a constructor that runs the class's own constructor on nothing and zero,
// then one override per method it can override, each handing the call over
});
return MethodHandles.lookup().defineHiddenClassWithClassData(standing, overridden, true)
.lookupClass().getDeclaredConstructor(InvocationHandler.class);
tls's certificates are interfaces, so they get proxies.
bugger's colony doubles a
java.lang.ClassLoader, an abstract JDK class with no interface to
hide behind, so it gets a hidden subclass written by the code above. Its
migration: sixteen lines, all imports, 51 tests green.
No agent. No constructor bypass: the subclass runs the class's own
constructor, and if that throws, carlin refuses the type by name. And Gemini's
ThreadLocal? Half right. Matchers really do run inside out, so
carlin keeps exactly one thread-local for them. Stubbing and confirming go
through a second stand-in instead, so a double never has a "mode."
Seven words you can't say in a carlin test
The library is named for George Carlin, who spent a career pointing out that most of our words exist to keep us from noticing what we're doing. Mocking has plenty. We don't fake objects; we "mock" them. When a fake isn't enough, we "spy": a fake that secretly does the real thing. A "deep stub" is a fake that hands you fakes, all the way down. An "inline mock maker" rewrites your classes' bytecode while they load. Soft language.
The agent's first instinct was fidelity: it made carlin answer unstubbed calls exactly the way Mockito's table does. I stopped it:
Our goal here isn't to copy mockito 1:1. We want a simplified and streamlined mocking library.
So there are seven words you can't say in a carlin test: spy,
doAction, atMostOnce, atLeast,
doubleStatic, doubleConstruction and
RETURNS_DEEP_STUBS. "Leaving out a bunch of mockito noise is
good." "We don't need mockito policing," so nothing nags you about unused
stubbings. And a confirmation that fails throws a Braindropping,
after his book.
That left the 625 tests the spike couldn't run, each one either coupled to Mockito itself or doubling something carlin won't: a final class, a record, a static method. This is where most people add the feature. I ruled the other way:
In places where the code design makes the simplest and most lightweight implementation of software.visionary.carlin (the mocking library) difficult, the prod design may be flexed to make carlin simpler (rather than the reverse).
A test that has to fake a static call, spy on the class under test or stub a chain of getters is telling you about a collaborator the class never named. The answer was the same every time:
you introduce an interface seam, make the prod code depend on the seam, mock it out, all this shit goes away.
A java.io.File became a Path. A
ServiceLoader became the Iterable it hands over. A
vendor session opened through a static method became a bean. Six concrete
repositories became Repository<T>. The tests got simpler,
and the production code got more SOLID. Mockito's real superpower had been
letting bad seams hide.
Same thread as bob
If this sounds familiar, it's the same story as the build tool, which I'll
write up next: stop renting complexity you don't need.
Mockito is a good library. It's also built to double anything, in
anybody's code, on any JVM since 2008, and every ounce of that generality
rode along in my test path whether I used it or not. Some people see that
glass half full and some see it half empty. I see a 5.68 MB glass for a 38 KB
drink. My modules return interfaces on purpose. I needed a
Proxy, a ClassFile builder and a few hundred lines of
bookkeeping.
It's not just humans, either. An agent working in a module that uses carlin can read all of carlin in one sitting: twenty files, the biggest 161 lines. Nobody reads Byte Buddy in one sitting.
Self-host the thing you depend on most, and make it small enough to understand. The anti-corruption layer is what makes that cheap. Build one around every third-party library you can't see the bottom of, and the day you want out, it's an import line. The tests were fine. The dependencies were fucked.
What's next
The doubles were never the point. They exist so a specification can
state what a piece of code promises. While Mockito was on its way out, I moved
a module's specifications out of src/test entirely: they now live
in the class they specify, as a nested enum Specification, and
the build runs them with a single call to a spex primitive. No code
generation, no test compilation, every line and branch still covered. That
gets its own story.
Here's where it goes. A specification that lives in production sources can be checked at more than test time. Informant runs it when the module builds. A compiler plugin can run it while javac is still compiling. An agent can run it when a class loads, and a background scrub can rerun it in production. Put that beside Contrax's compile-time contracts and you get software verified at compile time, at test time and at run time, from one specification, written once. All of it. 100%.
Stay tuned.