"Well I wanted to be different..."
...replacing Mockito, byte-buddy, asm, 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. 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, and a whole dependency management version versions. All of it, so a test could say "pretend this interface returns 3."
"I left a shit!"
Most engineering situations ignore tiny inefficiencies like this, with good reason. Hard drive space is so abundant, it doesn't seem worth taking the shit necessary to replace a dependency like this. But hard drive space is just the beginning. Then comes the weight of the dependency, which comes up when you're integrating multiple libraries. I ran into having to resolve between multiple versions of dependencies. Finally, mockito started emitting warnings because its agent uses an old approach and trying to warn me that I needed to prepare for future changes.
Leaving symbols to the symbol minded
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.
*/
"You ever been talking to yourself, and somebody comes in the room, and you have to make believe you're sane?"
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?
It explained itself by arguing: 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 Fable agent for a design pitch. It measured before it pitched. 2,027 places in the codebase'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 codebase's own tests on it:
648 lines. The same verdict as Mockito on 94% of the tests it could run, on the first try.
"People add extra words when they want things to sound more important than they really are."
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. With Claude Sonnet, not Fable or Opus. Not just 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.
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. We keep Gemini's
ThreadLocal for matchers. Since they really do run inside out,
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."
"Seat belts: high-tech shit!"
George Carlin 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.
Fable'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.
Here 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. A confirmation that fails throws a Braindropping,
because throwing Napalm and Silly Putty might make an agent press The Button.
The initial migration left 625 tests the spike couldn't run. Some were either coupled to Mockito itself or doubling something carlin won't: a final class, a record, a static method. Most people would add the feature. "People are really fucking stupid." I told the agent that Air Marshall Carlin said to go fuck itself.
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 constructor injected bean.
The tests got simpler. The production code got more SOLID. Mockito's real "superpower" had been
letting bad seams hide.
"All destinations are final. That's what it means: destiny, final. If you haven't gotten where you're going, you aren't there yet."
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. An anti-corruption layer makes that cheap. Build one around every third-party library you can't see the bottom of. 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.