-
-
Notifications
You must be signed in to change notification settings - Fork 358
Don't allow at_least(0) #133
Comments
As I mentioned in the other thread, I disagree with this change. There's an added benefit to having a configurable N value, namely that you don't need to care what "N" is, as long as N>=0. This allows you to implement both a "stub" like scenario and a straight up mock with the same interface. Consider the value for DRYing up specs that share the same behaviour except the N value, namely: %w(foo bar baz).each do |name|
it "should have its 'a' characters replaced with '_'" do
name.should_receive(:sub).with('a', '_').at_least(name.count('a')).times
# complicated setup and spec behaviour here, calling some method
# that replaces 'a's in +name+.
end
end Note that bar and baz will receive once, but foo should receive zero times. Disallowing this syntax means you have to have a special case just for foo, which leads to a more complicated test. I see no logical reason why this behaviour needs to be restricted, it seems like an artificial limitation to me. A mock shouldn't care how many times it is received. Given the functionality benefits illustrated above, there's use in this feature sticking around. |
@lsegal: I don't think that example is convincing. The test does not assert
It asserts 'foo is received zero or more times', which is equivalent to 'I do not care what processing @dchelimsky I'm not familiar with the code, but I could imagine Edit: on second thought, that parenthetical comment
is actually interesting. Isn't it reasonable to want to process an edge case that should not result in any observable behavior with the same spec that verifies observable behavior in other cases? In that case the line |
@Confusion you could use |
@Confusion by the way, it doesn't quite mean "I don't care what processing foo results in". If you were to write out the spec for foo individually, yes, it would mean that, and it would seem a little odd, but that is not what is being done here. The whole point is that in the context of batch specs, I'm not thinking about each individual matcher call. The overall spec should translate to, "I'm giving you a list of data, and I want I think it comes down to this: yes, this expression can be rewritten in a more accepted form, and yes, this would be weird if it were written out literally as |
@lsegal saying
So is @Confusion exactly # >= 0
at_least # >= 1
at_most # >= 1
@lsegal in terms of your argument about keeping specs DRY and how this change would force you to special case things, I'd argue that this is a good thing! 0 is a special case for BTW - I'm not 100% sold on this myself. Nobody has actually complained of being bitten by a false positive stemming from |
This. A thousand times, this. If 0 is a special case, it should have a test to draw attention to it. Special cases are important and shouldn't be hidden in code. DRY, as a principle, is about logic more than code. If your DRYing out your tests, you're probably doing something wrong. Furthermore DRYing can introduce coupling between concepts and should only be done for logic that is truly "logic-ing" the same (business) process. If two separate processes happen to (perhaps temporarily) have the same or similar logic, DRYing is still questionable.FWIW, I'm totally in favor of this issue's proposition. |
I do not think that Borrowing the example of @lsegal it "should have its 'a' characters replaced with '_'" do
# May pass, but it really just stubs the call.
# The code may indeed call 'foo'.sub('a', '_') and the test would pass.
'foo'.should_receive(:sub).with('a', '_').at_least(0).times
# Much clearer what is being tested.
'foo'.should_receive(:sub).with('a', '_').never
end The biggest risk of |
There seems to be a dangerous attempt to conflate the stylistic opinion that "this is not proper style" with the logical stance that "this does not make sense" in order to win what is effectively a stylistic argument. Again, I have to reiterate that saying "
This is not a fair statement. We're not discussing what makes logical sense to a computer (and it doesn't, by the way), we're discussing what makes logical sense to a human. The statement This is a simple set of questions that should clarify the boundaries of
We all answer "No" to Q1 and "Yes" to Q3 & Q4, obviously. However, if you answer "No" to question 2, you are contradicting RSpec's own functionality. As pointed out, an object can receive a message 0 times, via Furthermore, I call on RSpec's own method library to show that I question the new constraints on In this context, if the API allows for a bounding of messages such that Finally, since this is a debate on style, my opinion is this: RSpec should stay out of making stylistic decisions for its users. We are all consenting adults here, and we can identify when we break recommended idiomatic patterns in order to obtain certain functionality. Ruby would not exist were it not for this possibility. It's one thing to advocate for a certain style (in documentation, in best practice guides), it's another thing to restrict functionality because of stylistic expectations. If something is logically valid (as proven above), and, not only that, but also has an accepted equivalent method ( |
@lsegal this is not about style. "should_receive" means "if this doesn't happen there should be a failure".
Agreed that Your points about consistency suggest we should make all three of |
But that is style. Yes, it's a tautology, but tautologies are not logically incorrect, they are only stylistically superfluous. RSpec doesn't disallow users from writing tautologies anywhere else that I know of. You can happily write things like: :red.should == :red Except that in the case of What I've been meaning to ask is this: is there a technical reason why this syntax needs to go? Again, without a technical reason, this seems purely stylistic. |
We're talking about a DSL with human readable words in it that mean things to humans who read and write them. I've had a similar debate with @jimweirich over describe Thing do
it "expects something" do
thing = Thing.new
thing.should_receive :foo
end
end The reason is that in flexmock, In our case, we have two different words: |
This is exactly why I say style. I completely understand the DSL, but when I (a human) read "object should receive :message at least zero times", I (a human) understand that this is a stub. RSpec still might call this a mock internally, but the distinction is an irrelevant technicality by that point. RSpec shouldn't police how humans interpret the "English". Humans should perform that policing through their own conventions. This is where we might disagree, of course.
The concepts of a "stub" and a "mock" should be defined by their properties, not how they are declared. A certain "thing" is a mock if (in RSpec's context of should_receive) it requires a message passed to it. In other words, it's not a mock because you said I should point out, by the way, that the distinction between Jim's flexmock and RSpec don't seem as significant as you make them. Frankly, the only difference is the initial constraint applied to |
I hear you, but don't agree. If I write a method named
I think that, absent any further clarification, it implies exactly once, which is why that's the default. |
That's just not correct. Furthermore, in RSpec, 'should' has a very clear and distinct meaning. If something 'should' happen, it is a failure if it doesn't. The same should apply here: if an object should receive a message and it does not, that is a failure. |
Considering the semantics of a mock expectation, if it does not have a pass & failure case, it is not a mock expectation. This is the reason why we have stubs.
|
No, there is no additional technical motivation for this. |
A point that I feel has not been addressed in the discussion above, is what I noted in the 'edit' in my first response: that it seems convenient and practical to have something like %w[foo bar baz].each do |name|
it "should do something" do
name.should_receive(:method).at_least(method_determining_count(name).times
end
end work properly, even if one of the This discussion is now primarily about what the semantics of various constructs should ideally be, but I think we are losing sight of the practical implications of the proposed change. Perhaps this use of |
@dchelimsky @bjeanes, sorry, upon re-reading I realized I had missed an important word, let me re-phrase: An English statement "object should receive X" does not only imply "at least once". The important part is emphasized. I understand that "I should receive a letter from the bank every month" should mean "once" (or at least once). The fact is, however, I can also say, "I should receive any number of letters from the bank". This statement is unambiguous, and you would understand me if I said this to you. You would not say, "but, that
@justinko I understand this, but the point is that The problem, I think, is that there seems to be an ideological stance on what "should receive" should mean, and this is keeping us from having a practical discussion on the usefulness of a configurable N value in On the note of practicality, I should point out that just about every mocking library I've researched supports a mock expectation (not a stub) with "zero or more" times.
With all of these other libraries supporting [0,infinity[ cardinality, why does RSpec need to be different? It seems extremely trivial to support the case of N=0 (it's not even a special case). Ruby's mocha shows just how easy it is in the above rb file. |
I don't disagree with your practicality argument: it is easier for you to set things up in loops if rspec is a testing framework (there, I said it!). A message expectation is a form of a test. A test needs to have a success mode and a failure mode, otherwise it is not only not a test, but it is an impostor deceiving you into thinking it's a test, giving you a false sense of security that your code works as expected. It is a lie. You've argued that "should_receive" need not mean a message expectation, but we simply disagree. There is a mental model that we all build up about these things, and your mental model seems to be that Admittedly, I never noticed the agregious violation of this mental model perpetrated by As for "that's how everybody else does it", I call your attention to YARD, which strays from previous de facto standards. If we agree that mocks aren't stubs, and we agree that the difference is that mocks are tests and stubs are not, then all four of the frameworks you cite got this wrong too. |
Lots of tests can be lies. The difference is in this case, the lie also benefits from setting up a stub, so it's not just a lie; it's just a difference in interpreting behaviour. At least, I'm not using it as a lie. The more interesting question is: how many people are bitten by this lie as opposed to any other tautological logic misstep? My guess is that the number of people who have shot themselves in the foot with Also, even if it is a lie, RSpec doesn't need to yell, "I won't let you do this because you're not testing anything!". It can easily just warn me about this and let me silence said warning if I understand what I am doing.
I'll know better than to make sure my behaviour is within your world view before bringing future bug reports to your attention, then :P I certainly did not expect a regression report to cause the removal of a feature that has been working for 4+ years. Can't say I'm happy about that.
I'm very glad you brought this up. YARD might impose certain defaults on users, but there's a big difference between the way YARD does things and the mentality behind this proposal. In YARD, if we stray from the "default", we provide a working path for users to achieve the same results. In some cases, we support the "defacto standard" even though we don't think it's "correct". In fact, (I'd like to think) we never make decisions based in ideology unless there is a good technical reason, or if we just don't have the resources to implement something. In this case, there is no technical issue, and the feature exists, so there is no resources issue either. It's also important to realize that YARD is built around the concept of extension, so although you might not consider "write a plugin" to be a valid response, in our case it is. Note, however, that whenever we provide a "write a plugin" answer, there is always a supported public API to make said plugin. If you provided me a public API to make
Stubs are not tests, but stubs are used in tests. There's no reason for RSpec to police the distinction when it already supports both. The distinction is often subtle, and many times not even relevant. The example above is one of those cases. I propose that RSpec treats its users like adults and lets them decide what those distinctions are. By the way, I have no problem with documentation telling people they will burn in hell for using |
Actually, I think you are, but that's admittedly subjective.
Agreed. Probably not many, if any (I haven't heard of this particular problem).
Deprecation and mechanical turks it is!
Probably won't do this for this, but that is the same approach I try to take for all things rspec these days, so if we ever did actually remove it I'd be OK w/ exposing an extension for it. Also, although we haven't formally declared semver (we don't meet all the formal API requirements yet) we do honor it as much as is feasible (only bug fixes on patch releases, no intentional breaking changes on minor releases), so if we did decide to actually stop supporting this (or anything else) it would come in a major release. |
No, #131 was a real-world issue. We did some refactoring, then said to ourselves, hold on, that should have broken a spec. On looking at the spec, that was what we found. The coders intent was obviously (from the context which I haven't made public) that this particular method should be invoked once or more. However he coded 'should_receive ... any_number_of_times' and missed the fact that this allowed zero times. |
FWIW, a little extra background on the context behind my use-case: the should_receive's in our spec aren't the actual tests, they are just setup. In other words, we could easily replace all of the shr's with stubs and the test would still have a purpose, so they effectively are being used as stubs here-- it is not a lie. However, we also take advantage of shr's assertions to provide some extra sanity checks in the tests. In other words, we are providing some extra checks to "test the test"; if the tests pass but don't call on |
This change would remove an edge case where the api does not assert anything. I think it would be great to make the meaning of should_receive more consistent. I can't see why writing misleading code would be of any use. I believe its misleading as its general expected that should_receive means that something should be received. |
want to test methods being called on rails associations. class User < ActiveRecord::Base
has_many :facebook_accounts
end
class FacebookAccount < ActiveRecord::Base
belongs_to :user
end in my tests i do not care that So the tests should pass if:
|
@deepak -- I have no idea what you're asking or what the connection is to this issue. If you have a general question about how to test something, github issues isn't the place to do that. Stackoverflow is a better forum for that. Thanks. |
@myronmarston i thought, in this case facebook_accounts can be called at_least(0) times i sent a mail to rspec-users some time back, if that is ok ? thanks for responding |
I think you could do something like: fb_accounts = user.facebook_accounts
user.stub(:facebook_accounts) do
fb_accounts.should_receive(:build).with(some_params)
fb_accounts
end |
thanks @myronmarston that cleared on how a stub can be used. |
Was a decision ever reached on this issue? It looks like the consensus was that at_least(0) and any_number_of_times should raise a deprecation error when used but not be disabled. |
Closing because of #231 being merged. |
See #132 for background.
This statement doesn't really test anything:
It will pass if the object receives the message any number of times, or never really receives it. It is really just a stub.
Let's raise an error in 3.0 recommending the user use
stub
instead, and add a deprecation warning in a 2.x release with the same message.The text was updated successfully, but these errors were encountered: