tz-lom
November 16, 2025, 9:04pm
1
I could not find a package that would allow to temporary override function implementation, so I’ve made one: Mock.jl .
I’ve checked other packages before, here what I could find:
Closest to the concept would be Pretend.jl but it requires annotation next to function declaration.
Similar behavior have Mocking but it requires annotation at call site, which is something completely different yet quite interesting.
There is also SimpleMock.jl but it is “kinda broken” according to the readme and Cassette.jl on top of which it is built is also having troubles supporting 1.12
The API is quite simplistic at the moment:
with_mocked(old=>new) do
# new is substituting old
end
Take a note that there is no need to modify old to work with Mock.jl - if function can be overridden it can be mocked (only built in functions can’t be mocked).
There is also a macro @original which allows you to call original method (the old one which was before mock was applied).
At this moment macro is not 100% working - recursive calls do not work at all. In experimental branch with even more abuse of the Julia runtime I’ve made it to call old code, but then for recursive function I have a silly sequence of new -> old [recursion happens here] -> new -> old [may be recursive again] etc which is not what I would call reasonable, so more work is ahead.
Internally this package is built on top of Base.delete_method functionality which allows to temporary override implementation, because if method is deleted with this function then the old implementation is available again.
For the package name there are doubts though:
master ← registrator-mock-7b8b9345-v0.1.0-7db69a14e2
Thank you for submitting your package! This looks very neat to me, but there are… few stumbling blocks with the registration. The package seems like a potentially important ecosystem package, where I would recommend taking special care both about the design of the package and about the long-term maintainability of the project – especially with a very general name like this.
With respect the the former, it might be a good idea to [pre-announce the package in Discourse](https://discourse.julialang.org/c/package-announcements/60) to get some community feedback. There are a few existing solutions for mocking (like [Mocking](https://github.com/JuliaTesting/Mocking.jl) and [SimpleMock](https://github.com/JuliaTesting/SimpleMock.jl), but they seem to have to deal with a lot of technical complexity in the Julia language. It would be good to understand how you get around these technical issues and for there to be some community buy-in on the way in this new package is implemented. It would also be good to add some notes to the README about the design, comparisons to existing solutions, and possible limitations.
With regard to the latter, this package should probably be hosted in [JuliaTesting](https://github.com/JuliaTesting) (like the other two mocking packages I mentioned).
I'm not sure about the name… If the design of the package is sound, it seem okay to me, although it could be confused with `Mocking` (ironically, unlike the other "name similarities" that the bot found). We're also getting pretty strict about the "Name is not at least 5 characters long" guideline, though. A 3-letter package would have almost no chance of getting merged anymore, but I'm not sure where we stand on 4-letter names. Like the functionality, the name would be something to seek community feedback on, on [Discourse](https://discourse.julialang.org) and/or [Slack](https://julialang.org/slack/) (`#pkg-registration`)
Mock.jl violates community guideline of “at least 5 characters”.
What do you think about the name? Shall I leave it as it is, or you have a better suggestion?
4 Likes
Nice! I recall a comment during Juliacon that improvements in the world model (which functions are defined when) might soon improve mocking capabilities. The test ecosystem could use more ways to mock.
Naming is fun. How about Tempora.jl for temporary mocks. Or BaseMock.jl. Or ShadowCall.jl? I don’t know. Have fun with it.
Cheers,
Drew
tz-lom
November 18, 2025, 8:54pm
4
Those are really good names, however, looks like Mocking.jl is open to take this under the hood
opened 05:05PM - 18 Nov 25 UTC
The most common complaint about Mocking.jl has been that it requires modificatio… n of the source code in order to apply patches. As of Julia 1.12 additions such as `Base.delete_method` provide us with new options to apply function patches in new ways. This proposal documents an approach inspired by @tz-lom's work on https://github.com/tz-lom/Mock.jl for an approach to mocking without using call-site annotations.
## Apply patches via `eval`
We can use `eval` to add/override a function method. By using eval we can inject a user defined patch into a function's method table. Adding a method in this way will cause calling functions to be invalidated and call our patch. Restoring the original function method can be done by simply using `Base.delete_method` to remove the patch function.
Unfortunately, applying patches this way is not thread safe which can lead to some unexpected behavior when a patch is called outside of an `apply` block. To address this problem we can utilize a runtime check within the patched method which checks for a `ScopedValue` and conditionally calls the patched method when within the scope of an `apply` block otherwise they will call the original method.
## Calling the original method
With the removal of the call-site macro `@mock` we need a new approach for calling the original method from within the patch itself. To do this we can create the macro `@original` which utilize `Base.invoke_in_world` which would allow us to call the original method while a patch as been applied.
One tricky part here is that since patches can be reused in different apply blocks we probably want to utilize the world age number from the start of the `apply` block rather than the world age from when the patch was created. The advantage of this is that interative workflows using both Mocking.jl and Revise.jl should work better. If we took the world age from point at which the patch was created then user function changes updated via Revise would not be picked up when calling `@original` from within a patch in an `apply` block.
## Backwards compatibility with `Base.delete_method`
For removing a patch we can utilize `Base.delete_method` to remove the added method. As this is only available in Julia 1.12 we can need a solution which also works at least with the Julia LTS (1.10). Once again we can utilize `Base.invoke_in_world` to restore calling the original method:
```julia
f(x) = Base.invoke_in_world($prev_world_age, f, x)
```
We would generate these methods for each patch we applied. These methods would result in us calling the function using the world age from just before the `apply` block such that nested `apply` blocks should work correctly. As we leave each `apply` block we'll be defining a new "restore" method which will be hardcoded with the earlier world age.
When considering this approach I became a bit concerned about performance. Would it be possible multiple apply/restore iterations result in "restore" methods calling "restore" methods such that it would have a performance impact? I don't think think we'll have a problem here since as we leave each `apply` block the "restore" method would be calling an earlier world age such that these methods would typically call the original method directly. In scenarios where nested `apply` blocks have more and more specific patches then we could end up having a "restore" method calling another "restore" method with a more generic signature. However, this scenario seems rare enough that it won't be an issue.
The primary downside of this backwards compatible approach is that we end up polluting the method table with additional methods if a patch uses a different argument types than what was defined in the original function. This isn't a big problem as Mocking.jl is expected to be use primarily for testing purposes. There could be some packages which are testing results from `methods` which could see test failures. In those scenarios the package maintainers would just need to be careful to only create patches which match the signatures of the original methods.
## Backwards compatibility with `Base.invoke_in_world`
We plan on using `Base.invoke_in_world` in the `@original` macro as well as for some backwards compatibility when `Base.delete_method` isn't available. This function has been available since Julia 1.6 which allows us to at least support the Julia LTS (1.10).
## Backwards compatibility with `ScopedValue`
We are already using `ScopedValue`s in Mocking.jl but it's still worth mentioning this requirement here as this feature requires Julia 1.11 and we now rely on it in different way.
Previously, when we didn't have `ScopedValue` support we ended up falling back on a global value that wasn't thread-safe. This global was used by the `@mock` at the call site to determine whether to call the patch or the original function. With the new approach we have the same thread-safety limitations.
## Repercussions
In looking into applying this proposal I suspect the following will also occur as part of these changes:
- Removal of `@mock`: If there isn't any performance downside to this approach then we can remove this
- Removal of `Mocking.activate`: Was used to trigger invalidation at `@mock` call-sites
- Removal of `Mocking.nullify`: Only was needed for `@mock` call-sites
- Removal of `Mocking.dispatch`: Used for performing dispatch as if two functions had the same method table. Shouldn't be necessary as we can no just use Julia's method dispatch with world age.
- Nesting patch environments will no longer require them to be merged: We should be able to extend a function and rely on world age to temporarily apply patches
- `@patch` will need to construct the function which will be added (eval'd) later: The user will define there patch like they did before but we'll also create an function definition expression which perform the runtime check which determines if the patch or the original function is called. This function cannot use `args...` since this would impact method dispatch so we'll need to extract the actual args names when we call the original/patch functions. Probably want to add additional tests for this as I can foresee scenarios we pass in too many args into the original function.
2 Likes
aplavin
November 29, 2025, 1:43am
5
Nice package! I was looking for a way to substitute another implementation for an arbitrary function f in the callchain, locally within some scope. This package manages to do this in a zero-cost manner, right?
How does it deal with multiple tasks/threads, with e.g. one task using with_mocked(old => new) while others use the original definition of old?