-
Notifications
You must be signed in to change notification settings - Fork 266
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
Way to call base implementation when overriding a method on a partial substitute #622
Comments
Hi @logiclrd , thanks for the excellent issue report. 👍 Could you please provide some more context about the The feature itself is reasonable straightforward to implement, but we like to be careful adding anything to the main API as sometimes these things have unintended consequences. If you can provide more context about the |
Sorry, yeah, I should have been more explicit. The subject under test has code like this:
The client object is ancillary to the test, but is tightly tied to the way the subject is functioning. The return value of The objective I have been trying to achieve is to have the test capture the value of So, the path I was trying to follow was a partial mock of It's not always obvious if I overlook something in an explanation so let me know if I've missed anything :-) |
Thanks @logiclrd. Is it possible to show an example SUT and test of this scenario? Or is it too involved to reduce to a small case? @zvirja @alexandrnikitin Do you see any issues with adding something like in dtchepak@b7909cd? |
This code is untested, in part because I wrote it right in the text input on the GitHub issue page, but also because it makes use of both the
|
I like the idea of extending I see the following improvements we can make:
I would introduce Should we also rename |
@zvirja I have a couple of responses from my position here on the outside :-)
|
@logiclrd Thank you for the great explanation. That's a test scenario I see time to time. I'll try to explain my view on it. Now to your example, the "scope" notion is internal to the SUT, it is not exposed from SUT in anyway. We don't see it if we treat SUT as a black box, it's on different level of abstraction. The test code you provided looks over complicated to me.
To Nsub. In general, I see NSubstitute as a helper (syntax sugar) that helps to reduce boiler plate code, helps to keep things simpler and readable. I don't think it should be "jack of all trades" tool that can do some complex logic inside subs. It may provide some extensibility points for its clients though. Regarding |
Some further context on this -- we have a service that, among other things, includes two methods that drive a load-modify-store model for working with the data. But, the "load" part of this is also a general-purpose data retrieval method; there's no automatic assumption that if you use the "load" method then you definitely will be making changes and passing them back up. Because of this, the "load" method by default reads data from a read-only secondary instance of the database ( In many cases, this delay on the secondary isn't an issue, because in practice nothing has modified the data and it isn't actually out-of-date -- or if it is, it's out-of-date with irrelevant changes. However, we have a persistent service that responds to notification messages on a service bus regarding data changes, and we found that it would regularly read out-of-date data because the notification, sent after a change was made to the primary database instance, would arrive and get processed before the replication from the primary instance to the secondary instance had completed. For this reason, we introduced the concept of a "service client context" scoping system into the client. This scope allows headers to be assigned as ambient state that then apply to all requests made during that scope, and one of those headers can tell the service, "You need to do this read from the read/write primary, even though it is on its own a read-only operation." There is another service which has a method that updates data, and this update can affect links with data in this service with the load-modify-store model. During a major refactor, we had to temporarily comment out that second level update because we were migrating to a new platform that didn't support the communication technology the other service used. This meant potentially some updates would get missed, an unavoidable temporary situation until the downstream service also got migrated. It has since also been migrated, and so now we're reintroducing that second level update, converting the calling code to use the new client library. (The new client library exposes the same contract interface but handles the scoping completely differently; the old implementation did not wrap the scoping with any abstraction.) It is this change that I am unit testing. A key part of this unit test, then, is ensuring that the SUT, which is doing a load-modify-store against the other service, is in fact specifying the correct "intent" in the "load" phase via its scope, to ensure that the data upon which it is basing its changes comes from the read/write primary instance and not the potentially-out-of-date secondary. This is why the test has to care that the service client object, downstream of the SUT, sees an appropriate Ultimately I may be able to achieve this test by getting my fingers into the client object (which isn't part of the same project) and making its concept of scoping fully abstracted. This, then, would allow the use of full substitutes for the entire client layer downstream of the SUT. But, the current implementation exposes the scope as a "private" object that a caller cannot instantiate, because its lifetime, controlled by |
(I call it a "private" object in the sense of it having no public constructors; the type itself is not actually marked |
@logiclrd Thank you for the great explanation. It is more clear now. |
Thanks for the suggestion :-) So, the consumer using the SUT would supply it with the client appropriate to what it wanted to do, and if you tried to invoke the load-modify-store operation and you had supplied it with a |
Yes, correct, and the best part of Type DD is that it's compile time, meaning that there won't be runtime errors. |
Oh I see, you're imagining a SUT that can only work with a |
Thanks for these suggestions @zvirja . I tried pushing the type through |
Just to be clear, there is no guarantee that a base result even exists in a |
Add protected `GetBaseResult()` method to CallInfo. Create CallInfo<T> to calls that return results and expose `GetBaseResult`. This gets messy to push the generic all the way through the code, so am just using a cast in `Returns` extensions to handle this. This should be safe as if we are in `Returns<T>` then the return value should be safe to cast to a `T`.
Create CallInfo<T> to calls that return results and expose `BaseResult`. This gets messy to push the generic all the way through the code, so am just using a cast in `Returns` extensions to handle this. This should be safe as if we are in `Returns<T>` then the return value should be safe to cast to a `T`. Based off discussion here: nsubstitute#622 (comment)
Create CallInfo<T> to calls that return results and expose `BaseResult`. This gets messy to push the generic all the way through the code, so am just using a cast in `Returns` extensions to handle this. This should be safe as if we are in `Returns<T>` then the return value should be safe to cast to a `T`. Based off discussion here: nsubstitute#622 (comment)
Create CallInfo<T> to calls that return results and expose `BaseResult`. This gets messy to push the generic all the way through the code, so am just using a cast in `Returns` extensions to handle this. This should be safe as if we are in `Returns<T>` then the return value should be safe to cast to a `T`. Based off discussion here: nsubstitute#622 (comment)
Is your feature request related to a problem? Please describe.
I have a situation with a partial mock where I want to make use of the existing implementation, but intercept the call with my own override to catch its return value. That value is tied into the underlying mocked object's state, and it is called from within the SUT.
If I were creating the partial mock manually, then I would be able to write something like:
I haven't been able to find an official way to do this with a mock created by
Substitute.ForPartsOf<...>
.Describe the solution you'd like
I'd like an overload of the
.Returns
method that takes a callback delegate with an object richer thanCallInfo
that provides the ability to call the base implementation. Obviously, this method would result in an error if used on a full mock where there is no base implementation, but it would enable the above scenario via NSubstitute.Describe alternatives you've considered
I investigated the possibility of creating this mock manually, but the
Context
object that the baseBeginScope
returns needs to be known internally to the object being mocked for subsequent calls.I then dug into how NSubstitute handles call routing internally and came up with an alternative that is technically all based on public API, but feels really dirty & internally:
The above is pseudocode, so there may be typos, but I have verified that this exact pattern works in actual implementation.
The text was updated successfully, but these errors were encountered: