Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bclothier opened this issue Aug 11, 2019 · 9 comments
Open

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

bclothier opened this issue Aug 11, 2019 · 9 comments

Comments

@bclothier
Copy link

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()...

😆

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.

@stakx
Copy link
Contributor

stakx commented Aug 11, 2019

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?

  • At the Mock<T> level, much of the generic API is already built on top of non-generic static methods sitting in Mock (see how most generic methods in Mock<T> simply delegate to methods in Mock), it wouldn't be hard exposing those.

  • At the level of the individual setup, we're maybe half-way there. MethodCall is the internal class that you'd think of as a setup. It has a bunch of non-generic methods like SetCallbackResponse, SetEagerReturnsResponse, etc. Ideally, we would expose that functionality as some kind of Behaviors collection to which one could add a variety of Behaviors (similar to Moq v5). Mapping the current fluent API to a Behavior collection isn't straightforward because of verb ordering: Some verbs, like the obsolete .AtMostOnce(), appear very late in a fluent API setup, but need to end up in first position in a Behavior-like collection. So that collection would have to be more like a priority queue, but then we might need to expose each Behavior's priority value, which isn't super-neat. IT might actually be easier to not model the behavior collection as a priority queue, but as a regular list, which would mean a small breaking change (which is why I have avoided doing all of this so far).

  • Other kinds of setups like .SetupSequence already use a Behavior-like approach internally, we'd just have to ensure that all kinds of setups use a similar public API to add behaviors.

  • Finally, all this should be possibly without significantly affecting performance in a negative way. I've run some benchmarks about a year ago that clearly showed that Moq v4 would get slower if it used a collection of Behaviors internally. I'm not sure how much of an issue that would be for most users but it's still something to consider.

@dammejed
Copy link
Contributor

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
Copy link
Contributor

stakx commented Aug 12, 2019

@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
Copy link
Contributor

@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
Copy link
Author

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

  • As - This is necessary because 1) the types I'm dealing could have several interfaces and we can't control which interfaces will be actually used, and 2) because we build expressions dynamically, we have to do more work that C# compiler ordinarily does for us and one of those is the casting operation --- we cannot perform operations on IFoo.DoThis() when the mock is Mock<FooClass>.

  • Setup - Obviously. 😉 We use both the action and func versions.

  • Returns - At the moment, we only use the Returns<T>(T) overload. Maybe in future we'll use other overloads but we're at v0.1 so this will be fine for now.

  • Callback - For same reason, we only probably a simple callback. In our case, we refer to a unmanaged delegate which is basically an Action so we know very little; the rest is up to the user's code.

  • It - we basically use a very thin wrapper over the static It class to make it compatible, but beyond that, we simply transform from the wrapper to the actual It in our expression building.

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
Copy link
Contributor

stakx commented Apr 20, 2020

@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
Copy link
Contributor

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
Copy link
Contributor

stakx commented Apr 20, 2020

@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
Copy link
Contributor

Nice. Makes sense to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants