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
stubs/expectations hide false positives #227
Comments
I'd definitely like to get something in for rspec 3. You can see some previous work I did on this in #15 and also the conversation on the mailing list that led to that. @xaviershay created rspec-fire awhile ago, and I'm a huge fan. I'm currently leaning towards integrating it (or something like it) in rspec 3. There's already been a discussion around moving its functionality into rspec-mocks in xaviershay/rspec-fire#18. One thing I love about rspec-fire over the approach of better_receive (and tools like it) is that better_receive (as I read it, anyway) only works on partial mocks. I think that from a design perspective, you're far better served to use full-on test doubles, and rspec-fire gives you that interface-verification benefit while allowing you to use pure test doubles. That also allows you to run isolated specs w/o the dependency loaded. It also does arity checking, which is quite nice. So, in rspec 3 I'm thinking I'll probably roll in rspec-fire somehow, and possibly also provide a config option for the same kind of interface verification when doing partial mocking. |
+1. I only use partial mocks in very specific circumstances these days. Usually, I try to stub and mock objects playing a specific role, rather than being used just because they are instances of a specific class. In those cases, pure test doubles are preferable to partial mocks (IMHO). Occasionally, I'll write a shared example group that describes the role (e.g., the messages you're expected to respond to) and run them against the test doubles and the real objects playing those roles. This provides verification of the interfaces in a pretty elegant way too. In fact, the portion of my career where I was often using partial mocks, I wrote some pretty terrible code :) I found it did not force me toward smaller objects and encouraged me to mock simply to gain performance improvements in the test suite without addressing systemic design problems. |
Great to here! I think rspec-fire is awesome. Pulling it into RSpec 3 would be super helpful, but if you don't also check partial mocks it causes a difference in behavior when mocking a double as opposed to a "concrete object." Partial mocking is still widely used, systemic design problems or not. Checking the API on partial mocks would provide a consistent safety net. Those mailing-list/issue discussions are insightful, and give me some ideas for better_receive. I was already planning on doing arity checks, but I'd gotten some feedback suggesting to make this check a first class citizen in RSpec. It seemed more worth while to put the effort towards RSpec instead of a separate library if possible. Is there a timeline for RSpec 3? If I can help, I'd be happy to help out with responds_to config options and getting this feature out the door. |
I don't yet have a definite timeline for RSpec 3, but I hope to get started on it soon. Before starting it I'm trying to deal with all the outstanding github issues by fixing those that I can, or tagging things for |
I like this idea, but I was confused by 'mock' in the title of this. I think there's some scope for cleaning up the distinction between test doubles/mocks and method stubs / expectations... |
@alindeman while it's certainly true that in a perfect world, it would be nice to say "I should be able to send ANY object into this method as long as it responds to Performing a partial mock on an object drastically mitigates that problem IMO. Like anything, it's a tool and if one is using it just for performance improvements without fixing design issues, it's going to cause problems. Just as 2,000 acceptance tests can be slow and brittle. It doesn't mean acceptance tests are bad, just that they are being used incorrectly. I guess all that to say that I would love to see RSpec be able to partially mock (with verification) an object. See my proposed DSL here |
@jfelchner this is something @myronmarston and the rest of us will be looking at for V3 keep your eyes peeled for the announcement blog post ;) |
@JonRowe I'd love to start getting more involved. If there's something I can do, please let me know. Even if it's just documentation or issue triage. |
So here's what I have in mind for RSpec 3: Partial MocksFor partial mocks, I was thinking we'd add a config option: RSpec.configure do |rspec|
rspec.mock_with :rspec do |mocks|
mocks.verify_partial_mocks = true # not in love with this name, but you get the point
end
end With this option set to true, rspec-mocks will not allow you to set a message expectation or a stub on a partial mock that would change the object's interface in any way.
Test DoublesPure test doubles, on their own, do not provide rspec-mocks enough metadata to know what interface to verify against. I'm a fan of rspec-fire's approach to solving this problem and plan to port a version of it to rspec-mocks. New double declaration methods: # pass the class name as a string if you want this spec file
# to be runnable in isolation without `EmailNotifier` loaded.
email_notifier = class_double("EmailNotifier")
# ... or pass the class constant itself, as that'll give you a clear error ir you misspell it
email_notifier = class_double(EmailNotifier)
# `instance_double` can take a class constant or string name as well
another_double = instance_double(SomeOtherClass)
How this will differ from rspec-fire:
So...that's what I have in mind for RSpec 3. Feedback welcome! BTW, I'll be linking to this from the "The plan for RSpec 3" blog post I'm working on now, so I expect this'll get more eyes on it after I post that. |
@myronmarston this looks great. After your explanation of how Also, I know you're against partial mocks, but after thinking more on it, there are times when passing an object in (DI) just isn't something that I want to do. I'm sure you'll agree that there are cases (even more prevalent with those who are just getting started with mocking, in which case they don't even know what DI is) where the verbosity cost of DI isn't worth it. I'm not saying that you should coddle to that case, I'm just asking you to please keep it in the back of your mind. Of course this may not even be possible without monkey-patching the partial mock in which case I don't think that cost justifies the benefit of being able to do partial mocks. |
concrete example? |
Also @myronmarston that looks great! |
@xaviershay nothing that isn't pedantic for contrived. 😄 I know that it has happened to me in the past. Even without an example, DI is a really, really "Good Thing" but as with anything there are tradeoffs. One general example is if I have a class with a private method and that method is referencing another class in my library which is also private, all the extra noise required to:
all that is not worth the small amount of coupling to a class which, if it changes has a low amount of side effects. In addition to that, if you're doing DI just to make testing easier, you're probably going to be setting a default (which you want to do in most situations since it makes the API cleaner), and therefore, you're still coupling the class you're injecting into with the class you're injecting. class MyLibrary::Injected
def process_the_things(*things)
# Process Code
end
end class InjectUntoMe
attr_accessor :injected_class
def initialize(desired_injected = MyLibrary::Injected)
self.injected_class = desired_injected
end
def process(*things)
injected_class.process_the_things(*things)
end
end vs class InjectUntoMe
def process(*things)
MyLibrary::Injected.process_the_things(*things)
end
end I think that this case happens enough that we should try to handle it if we can. (I use "we" very loosely in this context since I probably won't be writing any of this. 😉) |
I'm not against partial mocks and have never said I am. I just prefer full-on test doubles. There are situations where I still use partial mocks. |
@myronmarston I didn't mean to misrepresent you. Sorry about that. I tend to write more in black and white terms when my intention is to be more gray. I'll try to watch out for that in the future. |
No worries :). |
@jfelchner in that example I'd do the latter case and double it like I don't see why you would want to use a normal "traditional" DI versus using a validated double are orthogonal concerns. This proposal only cares about the latter. Sorry but I still don't quite understand your argument :/ |
@xaviershay I think it just goes to show my ignorance and confusion in what all of the different options do since I completely missed the purpose of So I think at this point, functionality-wise, what @myronmarston outlined looks perfect to me. Sorry to take you both the long way around on this. The only other thing I'll say is: I have what I would consider to be an intermediate knowledge of this stuff and the API still caused me quite a bit of confusion, I'd wager a beginner to mocking would be even more confused. |
Ah, glad we're on the same page now :) Point taken re beginners, though I suspect better documentation and intro examples are what is needed. I don't see a more obvious way to communicate the concepts in code at the moment. (Would love to be proven wrong though!) |
While I think the
In spite of all my caution above, I think that constant stubbing (and particularly the
(I'm sure there are other good uses of it I haven't thought of, too).
You won't be able to use On a side note, I'm thinking that |
It doesn't looke likt it's been mentioned, so I'll mention it here. there's some pretty interesting stuff in this project: https://github.com/psyho/bogus |
I finished this a while ago. A complete re-write of stubbing and mocking. https://github.com/ryanong/spy I basically re-implimented all of rspec-mocks but in less than 1000 lines of code. |
Never meant to be a loc contest. Just check it out! There are some neat ways of handling the false positives, global object pollution. I loved rspec-mocks but I've run into too many false positive tests because of it too. So I tried to make a similar tool that would fire more errors. Check out the way I create mock objects to prevent stub objects bein only fantasies! Also it isn't almost rspec. I copied the rspec mock tests over and made sure mine passed. The only thing that isn't there is constant inheritance. |
@ryanong re preventing fantasy stub objects, please explicitly compare your approach to that of |
@myronmarston re naming of
Though maybe you'd need:
Which is accurate but kinda long. |
@aaronjensen -- Bogus does indeed look nifty. I've skimmed it and the contract tests in particular seems really cool. I don't have any experience with it yet, though. @xaviershay -- I think you're right that |
@xaviershay @myronmarston -- the my_double = replace_constant('Blah', class_double('Blah', my_method: true)) The duplication of This interface would retain the ability to support options for how the constant is replaced: my_double = replace_constant('Blah', class_double('Blah'), transfer_nested_constants: true) |
@pd: |
Duplicating the class name is unacceptable to me, that's why @myronmarston you could always do an optional double hash thing:
(I don't know if I like this, but it's an option.) I haven't really through it through, is there any reason you wouldn't want to transfer constants? Could we just make it non-optional? Maybe performance I guess. |
Added bonus: given most of the time you're replacing a constant with another class, make that the default behaviour of |
Throwing out some other options I don't like in case they inspire something:
You could "mix" options into the regular methods hash, then provide the two hash option if you actually want to stub it as a method.
Not sure if you could get it to work without an explicit kicker method though (default case). This is maybe my favourite of this batch:
|
This goes back to an original comment from @pat from over a year ago. I was originally planning on having it always transfer constants but @pat expressed a preference for not transferring them by default. I thought that the opt-in Since then, however, I've become kinda annoyed that the constants aren't transferred by default, and have thought about maybe making them do so in RSpec 3. I'm of a split mind about it, honestly.
What would the
Likewise, what would this do? I understand what replacing/stubbing a constant with a class double is for...but doing so with an |
Yes,
... but maybe you never actually should do that. In my code I can't find any instance of me doing this (always it's a I haven't used or felt the need to use |
@xaviershay What do you mean by run doubles in isolation? like within a block? or within a test? Or do you mean that when I create a double I effect the original Class? The way I create double is I actually create a new Class object that inherits from the Class you want to create a double from https://github.com/ryanong/spy/blob/master/lib/spy/mock.rb#L41 Then I include the Spy::Mock to this new class which will overwrite all the methods of the given class with a raise warning. You can actually re-use this entire "Mock Class" quite simply. class Book
AUTHOR = "Steinbeck"
end
MockBook = Spy::Mock.new(Book)
mocked_book1 = MockBook.new
mocked_book2 = MockBook.new
MockBook::AUTHOR == "Steinbeck" # You still inherit constants! Also there is a lot of discuss about the usage of replace constants and transferring nested constants. Do you use this feature often? When developing Spy I did a search on google and github and couldn't find any projects that used it. And if you create |
@ryanong I mean without the class you are mocking even loaded. See the |
@xaviershay Ah this is where I think we have different opinions. I originally let that happen in my first version but I changed it so I got less false positives. Lets pull up your So we are stubbing this |
You misunderstand @xaviershay's point. If you use If you haven't loaded the This allows you to rapidly develop one class, running only it's tests, with isolated unit tests, but then have the security of full verification when you run the entire suite. As a side effect it also allows outside in TDD, as you can create calls to methods that don't yet exist, yet this will cause a failure when you move down into the class. |
Ah thanks @JonRowe that is pretty neat. I don't like letting test call methods that don't exist yet but I can see with enough discipline it being quite useful. |
Outside in is often used by people doing more BDD style code than TDD, as you often start by describing what you want code to be and look like, and using that to dictate the design, rather than letting your existing code, or preconceptions about how something should work bubble up from the inside. |
I've used it before. I use it particularly with read me development. (How I designed the Spy API) http://tom.preston-werner.com/2010/08/23/readme-driven-development.html However whenever I hit that point at which I have to create a new method or interface I make that new api before I continue with what my dream api. This is to make sure my dream API is actually viable. Devil in the details kind of thing. I will probably add this feature into |
After seeing @xaviershay's example: replace('MyClass').with(some_fake_object)
replace('MyClass').with_instance_double(my_method: true)
replace('MyClass').transferring_nested_constants.with_class_double(my_method: true) I think that this is much more in the spirit of the direction that RSpec's syntax is headed. Small, verb-ed, non-monkeypatched, chainable DSL methods that do one thing well and are also very readable. One thought that I had was that, because the case in almost all instances would be to replace a constant with another class (as stated above) it could look like this: replace('MyClass').with(some_fake_object)
replace('MyClass').with_double(instance: true).allowing(my_method: true)
replace('MyClass').with_double.allowing(my_method: true) Benefits to this:
But once you see the above, you start to think, "Well if the instance version is bad practice and we don't really want to support it, why can't we simplify even further?" replace('MyClass').with some_fake_object
replace('MyClass').with double(my_method: true) In regards to nested constants, always doing it is less of a WTF moment and IMO should be the default. In the few cases where it's not needed, the DSL wouldn't look as good, but it's good enough for an edge case such as that. replace('MyClass', include_nested_constants: false).with some_fake_object PS: I'm not sure implementation-wise if I read all of the discussion but I hope I didn't misunderstand something. Thoughts? |
So it's been a while since I've actually looked at |
This is intended to be both API compatible with rspec-fire, and to completely obsolete it. Fixes rspec#227.
This is intended to be both API compatible with rspec-fire, and to completely obsolete it. Fixes rspec#227.
This is intended to be both API compatible with rspec-fire, and to completely obsolete it. Fixes rspec#227.
This is intended to be both API compatible with rspec-fire, and to completely obsolete it. Fixes rspec#227.
Mocks and stubs don't alert you when mocking/stubbing a method that the object doesn't respond to. This can cover up places in the code where methods are being called that are no longer defined. In my experience, it is unrealistic to rely on integration tests to catch this kind of error... Mocks and stubs are written far more frequently than integration tests.
I recently wrote https://github.com/se3000/better_receive as an add on to RSpec to cover this case and have gotten good results and heard many appreciative stories from the people that use it.
Is this a feature you would pull in if I wrote a pull request for it?
Options I've considered:
foo.better_receive :bar
)The text was updated successfully, but these errors were encountered: