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
ActionController::TestCase
: reset instance variables after each request
#43735
Conversation
52cbd32
to
f91164d
Compare
@controller.purge_new_instance_variables_from_previous_requests | ||
@controller.record_instance_variables |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we instantiate a new controller instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because then you can’t set ivars on the controller before any request runs.
While that’s not a good idea it’s possible and I’m sure lots of tests in the wild do it.
An example use case would be setting the current user.
Hum, my memory is fuzzy on this, but I think historically you weren't supposed to call two controller actions in a single tests. But to be honest I've seen this pattern a lot so it would be better to properly support it. |
Yeah I agree. You aren’t meant to but there’s nothing in the framework that stops you, so in practice it happens all the time. |
@ghiculescu yeah sorry about that one, I didn't really forget about this PR, it's just one of these things were you can't really think of a good solution. I'll think more about it and come back tomorrow. |
Thanks. Was hoping to sneak it into 7 but no worries if it's too late. |
If we find some decent solution I'll see with Rafael. What I mean by no good solution is that I can see issues both ways with restoring the state or not. |
f625f94
to
8467c85
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I was kinda busy today and I forgot to get back to this PR.
My main concerns is that some of the pre-existing instance variables may be mutated etc, so really this solution is far from being 100% reliable.
But if we're being reasonable, it's way better than not doing it, and your added documentation recommending not to make more than one request per test is a nice touch.
Let's add some changelog entry, and it's 👍 from me.
And apologies for the delays. |
8467c85
to
3136897
Compare
…uest `ActionController::TestCase` keeps a `@controller` instance variable, which represents the controller being tested. At the end of each request inside a test, its [params and format](https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal/testing.rb) are reset. But any other instance variables set in the test aren't reset. This creates a problem if you do something like this: ```ruby class UserController def show @user ||= User.find(params[:id]) render plain: @user.name end end ``` ```ruby test "gets the user" do get :show, params: { id: users(:one).id } assert "one", response.body get :show, params: { id: users(:two).id } assert "two", response.body end ``` The second assertion will fail, because `@user` won't be re-assigned in the second test (due to `||=`). This example is a bit contrived, but it shows how instance variables persisting between requests can lead to surprising outcomes. This PR fixes this by clearing all instance variables that were created on the controller while a request was processed. It explicitly excludes instance variables that were created *before* any requests were run. And it leaves the instance variable around until the *next* request in the test. This means that you can still do this: ```ruby test "gets the user" do @controller.user = users(:one) # assuming `Controller#user` users an ivar internally, you can set the ivar here... get :show_current assert "one", response.body assert_equal users(:one), @controller.user # and you can read the ivar here end ```
3136897
to
054fa96
Compare
No worries! No rush on this stuff, I appreciate the feedback. |
cc @rafaelfranca can we backport this this 7-0-stable? |
Done! |
`ActionController::TestCase`: reset instance variables after each request
``` Failures: 1) SorceryController with session timeout features with 'session_timeout_from_last_action' does not logout if there was activity Failure/Error: expect(response).to be_successful expected `#<ActionDispatch::TestResponse:0x0000557bab96c6d0 @mon_data=#<Monitor:0x0000557bab96c630>, @mon_data_...control={}, @request=#<ActionController::TestRequest GET "http://test.host/test_login" for 0.0.0.0>>.successful?` to return true, got false # ./spec/controllers/controller_session_timeout_spec.rb:146:in `block (4 levels) in <top (required)>' Finished in 2.52 seconds (files took 1.66 seconds to load) ``` [Starting with Rails 7.0, instance variables are reset between controller test requests](rails/rails#43735). This causes the second and subsequent requests to be judged as un-logged-in if there are no records in the DB. Putting a record in the DB fixes the failure.
``` Failures: 1) SorceryController with session timeout features with 'session_timeout_from_last_action' does not logout if there was activity Failure/Error: expect(response).to be_successful expected `#<ActionDispatch::TestResponse:0x0000557bab96c6d0 @mon_data=#<Monitor:0x0000557bab96c630>, @mon_data_...control={}, @request=#<ActionController::TestRequest GET "http://test.host/test_login" for 0.0.0.0>>.successful?` to return true, got false # ./spec/controllers/controller_session_timeout_spec.rb:146:in `block (4 levels) in <top (required)>' Finished in 2.52 seconds (files took 1.66 seconds to load) ``` [Starting with Rails 7.0, instance variables are reset between controller test requests](rails/rails#43735). This causes the second and subsequent requests to be judged as un-logged-in if there are no records in the DB. Putting a record in the DB fixes the failure.
* Change CI settings for support Ruby3.0+ Rails6.1+ Now, Ruby 2.7 is EOL, and Rails 6.0 is also EOL. So I changed the CI settings to support only those higher versions. Fix #340 * Move rspec-rails development dependency to Gemfiles Since the supported version of rspec-rails depends on the version of rails we use, move the description to each Gemfile * Fix the following failure ``` Failures: 1) SorceryController with session timeout features with 'session_timeout_from_last_action' does not logout if there was activity Failure/Error: expect(response).to be_successful expected `#<ActionDispatch::TestResponse:0x0000557bab96c6d0 @mon_data=#<Monitor:0x0000557bab96c630>, @mon_data_...control={}, @request=#<ActionController::TestRequest GET "http://test.host/test_login" for 0.0.0.0>>.successful?` to return true, got false # ./spec/controllers/controller_session_timeout_spec.rb:146:in `block (4 levels) in <top (required)>' Finished in 2.52 seconds (files took 1.66 seconds to load) ``` [Starting with Rails 7.0, instance variables are reset between controller test requests](rails/rails#43735). This causes the second and subsequent requests to be judged as un-logged-in if there are no records in the DB. Putting a record in the DB fixes the failure. * Fix failures of specs due to a change in the keyword argument specification fix failing tests like the following ``` 1) SorceryController using create_from supports nested attributes Failure/Error: @user = user_class.create_from_provider(provider_name, @user_hash[:uid], attrs, &block) #<User(id: integer, username: string, email: string, crypted_password: string, salt: string, created_at: datetime, updated_at: datetime, activation_state: string, activation_token: string, activation_token_expires_at: datetime, last_login_at: datetime, last_logout_at: datetime, last_activity_at: datetime, last_login_from_ip_address: string) (class)> received :create_from_provider with unexpected arguments expected: ("facebook", "123", {:username=>"Haifa, Israel"}) (keyword arguments) got: ("facebook", "123", {:username=>"Haifa, Israel"}) (options hash) # ./lib/sorcery/controller/submodules/external.rb:194:in `create_from' # ./spec/rails_app/app/controllers/sorcery_controller.rb:456:in `test_create_from_provider' # ./spec/controllers/controller_oauth2_spec.rb:44:in `block (3 levels) in <top (required)>' ``` * Remove an useless spec A spec fails like the following in Rails 7.1 ``` 1) SorceryController when activated with sorcery #login when succeeds sets csrf token in session Failure/Error: expect(session[:_csrf_token]).not_to be_nil expected: not nil got: nil # ./spec/controllers/controller_spec.rb:68:in `block (5 levels) in <top (required)>' ``` Delete this because it didn't seem like a useful test. * Add Changelog * Readd rspec-rails to dev dependencies for running tests locally * Do not remove the csrf test * Gitignore new sqlite3 files * Add pending clause for Rails 7.1 for CSRF token --------- Co-authored-by: Josh Buker <crypto@joshbuker.com>
ActionController::TestCase
keeps a@controller
instance variable, which represents the controller being tested. At the end of each request inside a test, its params and format are reset. But any other instance variables set in the test aren't reset. This creates a problem if you do something like this:The second assertion will fail, because
@user
won't be re-assigned in the second test (due to||=
). This example is a bit contrived, but it shows how instance variables persisting between requests can lead to surprising outcomes.This PR fixes this by clearing all instance variables that were created on the controller while a request was processed. This now matches the behaviour of
ActionDispatch::IntegrationTest
.Questions
Caveats
It explicitly excludes instance variables that were created before any requests were run. And it leaves the instance variable around until the next request in the test. This means that you can still do this: