"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:

A terminal message headed 'carlin: design pitch'. The spike is 648 lines on the JDK alone and gives the same verdict as Mockito on 9,153 of the 9,778 fleet tests it could run. Tables show who uses the vocabulary, what gets doubled, the two Java agents bob attached to every test fork, the 5.68 MB across four jars that leaves the closure against a 37 KB spike, and which production types account for the tests carlin would refuse.
The pitch, 2026-09-29: 648 lines on the JDK alone, the same verdict as Mockito on 9,153 of 9,778 tests. Every one of the other 625 was a double carlin refuses by design, or a test coupled to Mockito itself.

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.