Em 24-04-2011 23:39, David Chelimsky escreveu:
On Apr 24, 2011, at 6:25 PM, Rodrigo Rosenfeld Rosas wrote:
Em 24-04-2011 12:09, David Chelimsky escreveu:
On Apr 24, 2011, at 9:29 AM, Rodrigo Rosenfeld Rosas wrote:
I've started a fresh rails app with Employee belongs_to Company.
Here is the spec:
describe Employee do
example "stub should work with find(id)" do
company = mock_model Company
Company.stub!(:find).with(company.id).and_return company
employee = Employee.new company_id: company.id
employee.company.should == company
end
end
This ^^ is specifying Rails' behavior. There is a rather large test suite that
does this already, so I'd recommend avoiding this sort of example in a model
spec. Use-case specifics aside ...
Yes, I know, I just wanted to make my point. This is not a real test case.
And here is the result:
Failures:
1) Employee stub should work with find(id)
Failure/Error: employee.company.should == company
<Company(id: integer, name: string, created_at: datetime, updated_at: datetime)
(class)> received :find with unexpected arguments
expected: (1001)
got: (1001, {:conditions=>nil})
Writing any of the following is not elegant:
Company.stub!(:find).with(company.id, conditions: nil).and_return company
or
Company.stub!(:find).with{|id, *args| id == company.id }.and_return company #
find could be called like Company.find(1, 2, 3) which should return an array
instead
or
Company.stub!(:find).with{|id, *args| id == company.id&& (args.size == 0 ||
(args.size == 1&& args[0].is_a?(Hash)) }.and_return company
So, it would be great to add some syntax to rspec-rails like:
Company.stub!(:find).with_id(company.id).and_return company
or
Company.stub_find_with!(company.id).and_return company
I'm not sure yet what changes would be better, but the current implementation
makes it very hard to read specs like this that are so common when mocking
models.
What do you think?
Totally agree that this _should_ be easy. The problem with this approach is
that it would add a dependency from rspec-rails on Rails' internals. That means
that if/when the internals change, rspec-rails would break.
Not exactly, only if the Rails API changes which I don't think it will happen
for find in the near future.
What about a more generalized `with` method that only constrains the first n
arguments?
Company.stub(:find).with_args_including(company.id).and_return company
This would work with find(company.id) or find(company.id, anything_else), but
not find(anything_else, company.id). This doesn't guarantee solving the
problem, but it reduces the risk of future breakage because the likelihood of
active record changing that first argument to find is very small.
Thoughts?
The problem with this approach is that I don't want Company.find(1, 2) to
return Company.find(1), since it should return [Company.find(1),
Company.find(2)]...
Actually, I was thinking about something like this:
company = mock_active_model(Company)
Company.find(company.id).should == company
I mean, whenever a mock for some ActiveModel or ActiveRecord instance is
created, the class should also be stubbed to return that instance on find too.
There are basically two find alternatives: find(id[, options]) and find(id1,
id2...[, options]). These haven't changed as long as I can remember, so I think
it would be safe to include something like that on rspec-rails.
Do you agree?
Best regards, Rodrigo.
I definitely agree that it would be a nice to have if we could make it work
simply and reliably. That said, rspec-rails-1 has a long history of me having
to drop everything and do an urgent release immediately following a rails-2
release because internals that we never thought would change did.
rspec-rails-2, however, has survived 7 rails-3 releases unscathed, and I intend
to keep it that way. One major reason for this is a severely reduced dependency
on rails internals.
In terms of the API, how would you expect this to work?:
company = mock_active_model(Company, :id => 1)
Company.find(1, 2)
or this:
company = mock_active_model(Company, :id => 1, :published_on => 8.days.ago)
Company.find(1, :conditions => [ "published_on> ?", 7.days.ago ]
Etc.
Another concern is that named scopes defined with ARel do not go through the find method,
so this approach would not survive a refactoring from ActiveRecord finders to scopes
defined with ARel relations. Even if we can find "the spot" through which all
queries go, I would be resistant to depending on its stability.
There is a similar thread, right now, btw, in an issue raised on rspec-rails:
https://github.com/rspec/rspec-rails/issues/358. Feel free to review my
comments there and add yours. I'm not closed to any of these ideas, but I am
opposed to introducing brittle dependencies, or new APIs that open the door for
a battery of new expectations/requirements.
I agree with you. I do think we should keep it as simple as possible
relying as little much as possible on Rails itself.
Using your examples:
company = mock_active_model(Company, :id => 1)
Company.find(1, 2)
I would expect this to return [company, nil] unless Company.find(2)
actually returns something. Behind the scenes, Company.find(1) would use
the stub, while Company.find(2) would not.
company = mock_active_model(Company, :id => 1, :published_on => 8.days.ago)
Company.find(1, :conditions => [ "published_on> ?", 7.days.ago ]
I would expect this to return company, regardless on the conditions.
This should be explicitated on documentation of course. The idea is not
to replicate Rails here, but just to make it easy for constructing basic
object creation and simple relationships like belongs_to and has_many.
This should not work on scopes as it would be too complicated and
dependent on Rails internals.
I recognize that this last example is not that easy to decide on, since
the above approach could lead to some unexpected behaviors regarding
scopes for instance.
Maybe another approach would be defining something like that, although
this is a bit specific to Rails internals:
def mock_active_model(model, options={})
mock_model(model, options).tap do |m| # I know, I know, ignore tap
and the new hash syntax and you get the idea
model.stub!(:find).with {|id, *options|
id == m.id && (options.size == 0 || (options.size == 1 &&
options[0] == {conditions: nil}) )
}.and_return(m)
end
end
That way, the following call won't be stubbed:
Company.find(1, :conditions => [ "published_on> ?", 7.days.ago ]
I understand that this is not much desired because it creates a little
dependency on Rails internals, but I do see more benefits... On the
other side, supporting scopes doesn't pay off in my opinion.
Here is why I think that this is not a big issue. Rails find API hasn't
change as far as I remember. It just happens that Rails used to call
Company.find(1) instead of Company.find(1, conditions: nil). But this
was already possible before Rails 3. And the results should remain the same.
On the other side, if Rails decides to change this in some next release
to use another useless option, the changes in RSpec would be trivial, I
guess, although it might clutter the code with lots of "if
Rails.version==...". But the good news is that I don't think that things
like that changes very often in small releases, and bigger releases use
to happen in a long time. Also, the Rails core team decided to publish
some release candidate before each final release. This provides us the
chance to request them to rollback some change that would break RSpec if
it is simple for them to fix the issue...
I'm only asking for supporting the commonest case, because it is too
common and too hard to understand what the mock is doing as it is
implemented now...
class A; end
class B
belongs_to :a
end
When someone needs some scope or is avoiding the conventions, then I
don't think he or she would mind to write a bit more in mocks too...
I wish I could have some definite answer to your questions, but
unfortunately I don't and I understand your concerns and I know you also
did understand mine. Maybe someone else could enlight us with another
approach :)
Best regards,
Rodrigo.
_______________________________________________
rspec-users mailing list
rspec-users@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users