Add matchers start_with end_with #135

Merged
merged 5 commits into from Apr 9, 2012

3 participants

@jeremywadsack

This pull request provides a pair of new matchers start_with and end_with that are basically sugar to make specs more readable. They work with strings and arrays (and basically anything else that acts like a collection).

"A test string".should start_with 'A test'
[1, 2, 3].should start_with 1
[1, 2, 3].should end_with [2, 3]

I recognize that these are somewhat specialized and have noted that most built-in matchers are very, very generalized, but I found we were using a custom matcher like this often in our specs and thought it would be useful for others.

Reasoning

Currently, under Rails with ActiveSupport you can use the existing starts_with? and ends_with? methods to match the beginning and end of strings but they have some drawbacks. If used on the expected value then a failure reports an unhelpful message:

"A test string".starts_with?("Something").should be_true
# ==> "expected true but got false"

When used as a predicate, it doesn't quite read like english and is awkward:

"A test string".should be_starts_with("A test")

Finally, while we are using this for our Rails apps that have ActiveSupport for starts_with? and ends_with?, this implementation work independently of ActiveSupport so can be used outside if a Rails app as the rest of Rspec does.

Open to feedback as to whether you think these are valuable as built-in specs.

@dchelimsky dchelimsky merged commit 05d9853 into rspec:master Apr 9, 2012
@dchelimsky
RSpec member

I like it. I'm gonna a tweak a few things, but nice job.

@myronmarston myronmarston commented on the diff Apr 9, 2012
lib/rspec/matchers.rb
@@ -355,6 +355,20 @@ def cover(*values)
BuiltIn::Cover.new(*values)
end if (1..2).respond_to?(:cover?)
+ # Matches if the target ends with the expected value. In the case
+ # of strings tries to match the last expected.length characters of
+ # target. In the case of an array tries to match the last expected.length
+ # elements of target.
+ #
+ # @example
+ #
+ # "A test string".should end_with 'string'
+ # [0, 1, 2, 3, 4].should end_with 4
+ # [0, 2, 3, 4, 4].should end_with [3, 4]
@myronmarston
RSpec member

This looks like a typo. Shouldn't it be [0, 2, 3, 4, 4].should end_with [4, 4]?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff Apr 9, 2012
lib/rspec/matchers.rb
@@ -534,6 +548,20 @@ def satisfy(&block)
BuiltIn::Satisfy.new(&block)
end
+ # Matches if the target starts with the expected value. In the case
+ # of strings tries to match the first expected.length characters of
+ # target. In the case of an array tries to match the first expected.length
+ # elements of target.
+ #
+ # @example
+ #
+ # "A test string".should start_with 'A test'
+ # [0, 1, 2, 3, 4].should start_with 0
+ # [0, 2, 3, 4, 4].should start_with [0, 1]
@myronmarston
RSpec member

Again, this looks like a typo....[0, 2, 3, 4, 4].should start_with [0, 1] wouldn't pass, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston
RSpec member

I agree...great stuff! One suggestion: (which may be one of the things @dchelimsky was planning to change anyway?): I think that it makes more sense to use splat args so that it would be [0, 1, 2].should start_with(0, 1) rather than [0, 1, 2].should start_with [0, 1]. To me, the latter suggests it's matching an array of tuples, where the first tuple is [0, 1]. When you do indeed have a tuple (such as a for a 1.9 ordered hash), you could do something like hash.should start_with([some_key, some_value]).

Thoughts?

@dchelimsky
RSpec member

I like @myronmarston's splat args idea and will make that change. Among other things, it is better aligned with the include matcher that way. Not sure about ordered hashes - need to think about that a bit more.

I'm also making adjustments to the features and specs to better align them with their neighbors. Don't know if I have time to wrap that all up this morning before work, but coming soon ...

@myronmarston
RSpec member

Not sure about ordered hashes - need to think about that a bit more.

I think that'll just naturally fall out of this once you change it to splat args.

@jeremywadsack

@dchelimsky Thanks for reviewing this. Let me know if you want me to make the changes that @myronmarston identified in the rdoc.

@myronmarston as for tuples, I can see some value to that. It would be a clearer expectation of interface. Good idea. I didn't test the ordered hash idea, but it should fall through because it acts like an ordered collection.

That does bring up a good point that some sets that are not ordered (e.g. a 1.8-style hash) would fail because it responds to :[] but doesn't support a range index (e.g. hash[0, 3]). Is it better to somehow try to test for that or to just let Ruby raise an error? The former may be less robust but the latter may be more surprising.

@myronmarston
RSpec member

That does bring up a good point that some sets that are not ordered (e.g. a 1.8-style hash) would fail because it responds to :[] but doesn't support a range index (e.g. hash[0, 3]). Is it better to somehow try to test for that or to just let Ruby raise an error? The former may be less robust but the latter may be more surprising.

I don't think it makes sense to try to put special-case logic for 1.8 hashes, but maybe it'd be good to rescue the error ruby raises from #[0, 3] and give the user a more helpful error? That way it would potentially work for other types that have the same issue.

@dchelimsky
RSpec member

@jeremywadsack I'm working on a bunch of changes at once, though it's all mid-flight on my home computer which won't get much of my attention for a few days.

@dchelimsky dchelimsky added a commit that referenced this pull request Apr 10, 2012
@dchelimsky dchelimsky Change start_with and end_with matchers to take varargs.
- Clean up rdoc, features, and specs.
- Refactor the two matchers a bit.
- Add changelog.
- #135
2e0cdbc
@dchelimsky
RSpec member

Got to it sooner than I thought. @jeremywadsack let me know if you have any questions.

@jeremywadsack

Thanks @dchelimsky.

Also, noticed that the feature description has varying code style between end_with and start_with. In the former you use should end_with 1, 2 while the latter says should start_with(0, 1). Similarly the scenario for end_with.feature is, e.g. "string usage" but in start_with.feature you changed it to "with a string".

@dchelimsky
RSpec member

@jeremywadsack thanks - was trying to make things consistent and missed a couple of spots - will address before release.

@jeremywadsack

@dchelimsky should we also address the case where actual responds_to(:[]) but doesn't have ordered data? I believe that Ruby will raise an ArgumentError "wrong number of arguments (2 for 1)" which might be a little unexpected.

Incidentally, I'm not convinced that this works for a hash. In testing with ruby 1.9.2 hashes didn't accept numeric indexes and the docs for 1.9.3 state that the argument should be a key.

Per @myronmarston's example above I think that hash.should start_with([some_key, some_value]) will fail incorrectly because hash[0] returns nil.

I can give a stab at this tonight and see if I can come up with something a little more expected.

@dchelimsky
RSpec member

@jeremywadsack have at it.

@jeremywadsack jeremywadsack added a commit to jeremywadsack/rspec-expectations that referenced this pull request Apr 11, 2012
@jeremywadsack jeremywadsack Added more helpful message when actual has #[] but does not support #…
…[0,3]

Pull request #135
34e4285
@jeremywadsack

So basically this puts a message in place (feel free to edit for consistency of voice) for things like hashes that don't support ordered index or elements.

Without going down custom code for hashes I didn't come up with a generic way to actually allow one to match the first key of a hash because hash[0] always returns nil. I think this is probably good enough.

@myronmarston
RSpec member

@jeremywadsack -- sounds fine. My original comment about hashes wasn't intended to mean we should add code to specifically support hashes; rather, I was just thinking about a collection of ordered tuples (which could just be an array-of-arrays, and in that case, this matcher should work fine, right?), and mentioned a hash as one example of an object that behaves like an ordered collection of tuples on 1.9.

Thanks for following up on this!

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