devlooped / moq

The most popular and friendly mocking framework for .NET
Other
5.87k stars 800 forks source link

Provide non-generic API to support runtime type mocking #887

Open bclothier opened 5 years ago

bclothier commented 5 years ago

Normally, Moq uses generic APIs which is a great thing for ensure strong-typed, refactor-friendly mocking API. However, when the scenario is that we need to mock a runtime type, we can descend into a hell of Expressions trying to use the generic API.

The proposal is to open the non-generic methods as an API on an interface that is not easily accessible. In @stakx 's words:

Oh, I'd make sure that the low-level API is hidden and not something you'd casually stumble upon. Perhaps something similar to how .Protected() works; that is, you'd have to explicitly opt-into it via e.g.:

using Moq.Plumbing;

...
mock.DescendIntoDangerousPlumbingAPI()...

:laughing:

The scenario for wanting to mock a runtime type is to provide support to other languages outside C#. In my case, it's VBA language and we have a prototype here which is for VBA language. The users would be able to write some mocks for the unit tests in VBA which would use Moq underneath.

While we could have made our own mocking framework, that would considerably enlarge our project's scope and we'd rather not have to get into whole journey of learning how to write a good mocking framework but rather ride the coattails of the developers who already invested so much into building an excellent mocking so that we can only focus on providing an effective implementation in our project.

Back this issue Back this issue

stakx commented 5 years ago

I think exposing an "SDK layer" in Moq v4 would be fantastic and worthwhile, but I should probably add a few words to justify the effort required to do, especially when Moq v5 is waiting just around the corner: We should only consider exposing the "plumbing" / SDK layer of Moq v4 if it is reasonably well-architected and well-designed. I've been doing refactoring work for the past two years or so in that general direction, however we're not quite there yet. But being able to make that layer of Moq v4 public, in my eyes, would be a sign of good code quality.

So while I definitely want to do this, some work remains to be done, and it's not priority compared to e.g. bug fixes. So if that work ends up being mine, to give you a rough timeframe, don't expect to see results before approx. mid-2020.

More specifically, what remains to be done?

dammejed commented 5 years ago

Just giving a "Me too" to supporting exposing some internals (especially for creating setups) for use in 3rd party libs. I maintain a DSL that supports mocking via Moq (for much the same reasons that @bclothier mentioned), and currently, there are some things can only be accomplished via reflection calling into internal members of Moq in order to skirt around the limitations of the exposed API. It's not ideal, given that changes to the implementation internals in Moq require tedious manual work to fix.

I feel like it's a fairly niche case, of course, but wanted to add my hat to the pile.

stakx commented 5 years ago

@dammejed: Cheers for the feedback. Can you give a brief summary of the Moq internals you're accessing through reflection? Just to get a better idea what you'd want to become public in some way or another.

dammejed commented 5 years ago

@stakx Sure. The DSL and its useage are proprietary, so I can't share the code, but the general idea is this:

There are some generated expressions which are too complicated for Moq's expression visitor to understand and extract expressions from, so that it can match the called method and generate expression argument matchers. The DSL I maintain is able to extract the method and argument expressions, then simplify them to something understandable by Moq. It then directly creates the InvocationShape, MethodCall, and (Non)VoidSetupPhrase<> instances with these extracted parameters, and adds them directly to the Mock's Setups. This allows the remainder of Moq's internal plumbing to manage these somewhat complicated expressions.

The rest of the code is able to rely on the existing .Returns(), .Callback(), etc provided by the ISetup<> interfaces and their contstituent parts, but it's generating and adding the setup that requires the hackery.

bclothier commented 5 years ago

Just to provide some data point. Currently, I'm using those methods:

There are a number of items such as Raises that might be incorporated in some future but realistically, 90% of functionalities can be expressed with those entry points alone, and would be a good starting point for a SDK, I think. I doubt we might end up using all the capabilities of Moq anyway.

stakx commented 4 years ago

@bclothier @dammejed I've finally made some progress towards an untyped API; see #1002. After some internal refactoring, this sort of grew quite easily on top of existing code. It may not be precisely what you need but perhaps it would take you closer to where you want to be? Any feedback is appreciated.

dammejed commented 4 years ago

Thanks for doing this, @stakx!

I took a very brief look, and it looks promising for my usecase!

One suggestion that could potentially make it easier for me to migrate-- I mentioned before that I was relying on the existing .Returns, .Throws, etc statements after extracting the correct InvocationShape etc.

I don't necessarily need those to exist, but with my current infrastructure, the behaviors and the invocation shape are supplied at different times (e.g., with a fluent-style setup, as in the base moq library).

One way I could emulate that with the behavior-based extensions would be to allow adding more behaviors after the fact, e.g.,

mock.Setup(lambdaExpr, new Behavior[] { new CoolBehavior()})
         .WithBehavior(new EvenCoolerBehavior());

In the current implementation, a behavior-based setup is immutable once added. Supporting mutating the behaviors after the fact might ease the transition for me.

Not sure if that's counter to your design goals. Let me know.

Then again, I could always write a behavior that fully encompasses any customizations I would otherwise make after the fact and mutate it myself, I suppose.

I'll make a deeper investigation this week and come back.

Thanks again!

stakx commented 4 years ago

@dammejed that's definitely something that should be possible! While this could be done now for BehaviorSetup, my thinking was that perhaps it should wait until all internal setup types (MethodCall et al.) have been converted to a Behavior pipeline approach. Then we could open up all setups' pipelines via a single, consistent API, e.g. via a modifiable IBehaviorPipeline Behaviors { get; } collection property sitting on ISetup (which in turn are now accessible via mock.Setups):

mock.Setup(...);
var setup = mock.Setups.Last();
setup.Behaviors.Add(...);

On the other hand, if we enabled this now just for the new setup type, we'd probably have to expose a IBehaviorSetup : ISetup which may later become redundant.

dammejed commented 4 years ago

Nice. Makes sense to me!

github-actions[bot] commented 2 weeks ago

Due to lack of recent activity, this issue has been labeled as 'stale'. It will be closed if no further activity occurs within 30 more days. Any new comment will remove the label.