rspec / rspec-support

Common code needed by the other RSpec gems. Not intended for direct use.
https://rspec.info
MIT License
99 stars 103 forks source link

Enhance ObjectFormatter to transcribe values as their memoized let blocks or instance variable names #587

Open walski opened 10 months ago

walski commented 10 months ago

Subject of the issue

I am constantly spending time answering the question "which object is this?" when writing specs.

This is especially true for expectations on arrays or hashes, where one often deals with more than 1 value.

An example for this would be:

@one = Something.new(1)
@two = Something.new(2)
@three = Something.new(3)
@four = Something.new(4)

one_two_three = [@one, @two, @three]

expect(one_two_three).to contain_exactly(@one, @two, @four)

This would then report an error similar to this:

Failure/Error: expect(one_two_three).to contain_exactly(@one, @two, @four)

       expected collection contained:  ["#<struct Something id=1>", "#<struct Something id=2>", "#<struct Something id=4>"]
       actual collection contained:    [#<struct Something id=1>, #<struct Something id=2>, #<struct Something id=3>]
       the missing elements were:      ["#<struct Something id=4>"]
       the extra elements were:        [#<struct Something id=3>]

In this case it's pretty straight-forward to figure out the diff and the objects causing it. But this gets convoluted very fast with real world (e.g. ActiveRecord) objects.

Instead I would love if this would resemble something like this:

Failure/Error: expect(one_two_three).to contain_exactly(@one, @two, @four)

       expected collection contained:  [@one, @two, @four]
       actual collection contained:    [@one, @two, @three]
       the missing elements were:      [@four]
       the extra elements were:        [@three]

It should be fairly easy to do that at least for instance variables and let-blocks.

Would you be open to a PR in that direction? What would be a good approach to do this? Should this be an opt-in configuration? Should it be not in rspec-support at all and live in it's own gem?

Would love to hear your feedback :) Thank you!

Thorben

JonRowe commented 10 months ago

:wave: I'm open to this, what would you propose happens if one or more of the elements is not objectively identical to the value from a let or instance variable? What happens in the case of mutation?

Some matchers, like I think the one you've used as an example provide their own diff rather than relying on rspec-support how do you propose to implement this for those matchers?

walski commented 10 months ago

Hey, thanks for getting back!

what would you propose happens if one or more of the elements is not objectively identical to the value from a let or instance variable?

It should fall back to the representation that is used today (a.k.a. that inspect-like string).

What happens in the case of mutation?

I'm not sure if I understand that correctly. You are thinking about something like this:

# Assume a Rails request spec just for the sake of the example here

@betty = SomeActiveRecordModel.create(name: "Betty Smith")

# some code that calls a Rails controller and
# that controller modifies the record and sets the name
# to "Paula Jones" and returns it's ID as JSON

returned_id_from_response = JSON.parse(response.body)['id']

expect(SomeActiveRecordModel.find(returned_id_from_response)).to eq @betty

in which case the records would be equal to each other in ActiveRecord's logic due to them pointing to the same row in the database but would represent a different state of that row? I feel like that is a super bad example. Can you elaborate more what you really mean here? 😂

Some matchers, like I think the one you've used as an example provide their own diff rather than relying on rspec-support how do you propose to implement this for those matchers?

My understanding of the RSpec codebase is shallow at best, so I'd defer to the experts here. But in general I'd say this could be a process of adding such capability step by step in all the necessary places? And if this is not available everywhere from the get go, so be it. I think starting with these "container matchers" for arrays and hashes might be the best place to start. It's a place where you almost always deal with multiple objects, which makes it very cumbersome to work with the lengthy serialization of them in an inspect-like fashion.