-
-
Notifications
You must be signed in to change notification settings - Fork 837
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
Improve internal object relationship graph for easier testing #1164
Comments
I personally found it very hard to write isolated tests for
Wouldn't it make sense to have interfaces for the type(s) you have mentioned, so one could actually write fully isolated unit tests, when required? |
I did start out with an interface Nullable Reference Types really aren't a fan of having a default constructor in a type that doesn't set things up correctly. It also means you couldn't use readonly properties for the things that really shouldn't be changed. Perhaps we could have a derived mock type from |
I think it depends on what you're testing. Like, yeah, testing authentication stuff can be challenging. But, like, if you have a controller that stores something in
Well, yeah. Maybe. I mean, some of the things have abstract base classes already; we could use those with some minor updates. In some cases we have public interfaces for end user/application consumption but abstract classes for internal use. Of course, if they're for internal use, we could update them to be more usable for internal testing. I don't have exact suggestions on what to do right now. I think part of this is to look at what's there and see if there are things like:
I was just setting up the tests for some of the diagnostics work I'm doing and while it was easy enough to test at the very top, black-box level, it was really hard to set up tests for the internals and that seemed like a sorta red flag. |
Ooooo this is a good point. I'm not sure how to work around stuff like that. |
Most of the recommendations are either:
I actually contributed a section to the .NET docs about this: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies#late-initialized-properties-data-transfer-objects-and-nullability |
Perhaps the abstract base classes should use abstract properties along with their default implementation logic? Or maybe that last bit, with the I could totally get in the weeds on this in the diagnostic work so I'm not going to (see: For example, I see Point being, I don't know if we have to go full-bore into perfect testability, but maybe there are some things that could be cleaned up a little to just make it easier. Maybe? |
Agreed that it's definitely worth taking a look at it and seeing if it can't be improved. The pipelines type relationships were designed quite...iteratively...so would be good to have another look. |
I'd like to look into it this weekend a bit if you guys don't mind. About time to get a better understanding of internals anyway. |
@alsami That'd be awesome. Go for it! |
Small update: Related because of the missing tests: I've asked myself, if it's okay if we were to add I'd wait for #1169 to be merged and add it to the pipeline, if you guys don't mind @tillig @alistairjevans |
I'm definitely in favour of measuring our coverage, provided we don't rely solely on those numbers when evaluating PRs. I've done this before in my AutoStep tests, with that same report generator, and it's a really handy report. Bonus points for getting it included in the PR check results (I've seen that done elsewhere, but not sure how you get AppVeyor to output a report as a CI check...). |
Hehe, will go for the minimum now and build based on that. I am sure we can do that. On gitlab it's possible to add some regex to show the regression of coverage within the PR's, I am sure there is a way for AppVeyor as well. |
Here are a couple that are sort of annoying that I'm hitting right now trying to get the DOT graph diagnostics tests working: In order to report success or failure on a request/operation, I need a I can't Now, granted, I can create some test derived versions of these that basically just pass along the constructor parameters, but it'd be nice if it was easier. I feel like I'm sort of fighting the framework to write an extension and get it tested. I do see the Autofac.Pooling stuff didn't require quite as many gyrations, so maybe it's just inherent in the type of extension I'm working on and there's nothing we want to do about it... but it'd be nice if it was easier. |
Correction: I'm totally blocked because the I'll have to submit a PR to get a couple of these things unlocked a bit for testing. |
What would be a quick solution for that? Make the constructor public? I guess it was intended to make it internal. |
I don't know. Maybe make it protected? I haven't looked at it yet, trying to wrap up some other stuff. Might not be today. Day job and all. |
Can do that quickly. |
I don't know if that's the right solution. It's not a recommendation, it's a total stab in the dark. As internal, you can call it directly from other internal things. As protected, you're limited to the inheritance chain. It needs more time dedicated to it than what I'm able to give it literally right now, sorry. |
I am just playing with the latest sources a bit. Seeing now what's the problem with Basically for this class, to make isolated testing for public interface IResolveRequestContext : IComponentContext {
// public stuff like properties in here.
} Now for instance the extensions of Before I start doing that, anyone has a better idea or something that speaks against using an interface for that scenario? var diagnostics = new DiagnosticListener("SomeDiagnosticListener");
using var container = new ContainerBuilder().Build();
using var scope = container.BeginLifetimeScope() as ISharingLifetimeScope;
var operation = new ResolveOperation(scope!, diagnostics);
var request = new ResolveRequest(
new TypedService(typeof(string)),
new ServiceRegistration(Mock.Of<IResolvePipeline>(), Mock.Of<IComponentRegistration>()),
Enumerable.Empty<Parameter>());
var context = new ResolveRequestContext(operation, request, scope, diagnostics);
diagnostics.MiddlewareStart(context, Mock.Of<IResolveMiddleware>()); |
I'm ok with defining an interface that exposes the necessary properties (I believe that was how it was originally, but the presence of some behaviour in the context object prompted a change to an abstract class instead). |
I'd still keep the abstract class, just change all parameters to use |
It might be one of those "try it and see how it feels" deals. |
I'm OK with it but... it feels like an over-abundance of abstractions. We've got the actual I think if we switch to an interface, then the public base/abstract class is not interesting. If we keep the base abstract class, perhaps the required properties become abstract instead of virtual or something. I'm just not sure about having both the abstract and interface. |
As far as I remember @alistairjevans recently mentioned that he wanted to use it as in extension point in other libs. |
Well, right - but if it's an extension point then the constructor can't be internal, which is the whole issue with that particular class. I'm happy to have it remain if the constructor is also usable outside core Autofac, which would also, then, negate the need for the interface. The simple test would be: In a non-core-Autofac library (not the unit tests, either, which have access to internals) can I make a derived class of this thing? |
That's a valid point. I am gonna go for the interface everywhere solution, removing the base-implementation and making |
If we're going interface everywhere, shouldn't I.e. perhaps the only public item should be the interface. |
Sounds good, I am gonna give it a try now, plenty of stuff to adjust here and there. Will submit a PR as soon as it builds again... which might take a while 😄 |
|
@tillig, now that the names and structures are stable, are you unblocked on the DOT graph implementation? |
On a related note, I'd like to do the merge into develop this week so we can start looking at the integration packages and the remaining style changes; @alsami, are you ok to leave your unit test changes until we're on develop? |
Yes sure. I wanted to start with the remaining stuff somewhere this week. Just let me know when you have merged to develop, will wait till then. |
I didn't know if it was all stable yet. Yeah, if it's good to go I'll get back on the DOT stuff. I think merging to develop is probably fine. I can't see that we'd be issuing any 5.x fixes between now and 6. |
Looking at the tests, they use the implementation of |
The changes that were made were a definite improvement. Being able to mock stuff is a great change to what was going on.
private static ResolveRequest MockResolveRequest()
{
return new ResolveRequest(
new TypedService(typeof(string)),
new ServiceRegistration(Mock.Of<IResolvePipeline>(), Mock.Of<IComponentRegistration>()),
Enumerable.Empty<Parameter>());
} Should we look at messing with it? I mean, it's just slightly more difficult than it needs to be, but I also don't know if lots of folks will be writing tests at this level so maybe it's good enough? |
Yeah, if it's hard to test or not as easy as it should be, I guess making it easier makes sense. Putting it on the list and will look into it this weekend. |
I'd propose that we draw a line under these changes now. v6 is pretty stable, and we're mostly doing tidy up in prep for a release. I'm happy to take any test updates/improvements, but I think we're probably done with any further internals refactoring. |
I'll buy that. It's been improved and I'm not sure I have any actionable suggestions on further changes. |
Cool; I'm going to close this then. Thanks @alsami for all your work on the changes. |
Problem Statement
In creating (internal) unit tests for new features you sometimes need to create new instances of things like
RequestContext
(orRequestContextBase
),ResolveOperation
, and so on.Unfortunately...
This makes setup for tests somewhat fragile.
Desired Solution
I think we should analyze the relationships between things like:
...and see if there can be some cleanup/simplification of the relationships between these things.
Alternatives You've Considered
As noted, I've tried using Moq to mock these things but they don't have zero-parameter constructors. The only other option is to try to create all of these things.
Additional Context
Here's what it looks like to try and create a
ResolveOperation
, aResolveRequestContext
, and anIResolveMiddleware
for testing some of the diagnostics:I think the
ResolveOperation
construction is reasonable. However, I see thatResolveRequestContext
requires three things in the constructor and... maybe it needs a zero-parameter constructor and settable properties.By way of comparison, if you want to test something in ASP.NET Core,
HttpContext
(the abstract class) has a zero-parameter constructor so it can be mocked andDefaultHttpContext
can be newed-up so if you need something concrete without mocking you can do that.I think over time, as we've added
ResolveRequest
to the system, added pipelines, and done some major overhaul on the internals this has sort of grown and changed organically. If there's a way to clean it up and make it easier to work with, I think it might be more flexible for better testing as we add new features.The text was updated successfully, but these errors were encountered: