Skip to content
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

Introduce ApplicationTestCase #28243

Closed
wants to merge 1 commit into from

Conversation

gmalette
Copy link
Contributor

@gmalette gmalette commented Mar 1, 2017

I understand this PR isn't complete; I'd like to see if the idea gets traction.

Summary

All Rails test classes extend ActiveSupport::TestCase. This makes it
very had for applications to customize the test inheritance; they simply
have to reopen/freedom-patch ActiveSupport::TestCase.

Having a class implemented by the application would make it much more
straightforward. It's also in line with the way Rails has moved from
ActiveRecord::Base to ApplicationRecord, ActiveJob::Base to
ApplicationJob, etc.

All Rails test classes extend `ActiveSupport::TestCase`. This makes it
very had for applications to customize the test inheritance; they simply
have to reopen/freedom-patch `ActiveSupport::TestCase`.

Having a class implemented by the application would make it much more
straightforward. It's also in line with the way Rails has moved from
`ActiveRecord::Base` to `ApplicationRecord`, `ActiveJob::Base` to
`ApplicationJob`, etc.
@rafaelfranca
Copy link
Member

rafaelfranca commented Mar 1, 2017

I like the idea of generating that in the application but the framework should not use internally. The classes you changed here are actual subclasses of ActiveSupport::TestCase and they should not depend on something that is application specific.

@@ -1,7 +1,7 @@
require "active_support/test_case"

module ActiveJob
class TestCase < ActiveSupport::TestCase
class TestCase < ApplicationTestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea of ApplicationTestCase applies to Rails projects, not the Rails test suite itself.

@@ -25,7 +25,7 @@ module Generators
# destination File.expand_path("../tmp", File.dirname(__FILE__))
# setup :prepare_destination
# end
class TestCase < ActiveSupport::TestCase
class TestCase < ApplicationTestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also generate ApplicationTestCase for new Rails apps

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But ApplicationTestCase make not sense since the application may have view tests, controller tests, integrations tests, model tests. If we add new test case superclasses we would need ApplicationViewTestCase, ApplicationModelTestCase. I think this is a problem to the application solve. If they are doing things to ActiveSupport::TestCase just because it is shared across all they test cases probably they want to add only in the places the thing is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still means that Rails must provide the structure for that in generators, yes?

@gmalette
Copy link
Contributor Author

gmalette commented Mar 1, 2017

@rafaelfranca this is how I see the current hierarchy of Rails test:

untitled diagram 1

As implemented in this PR:

untitled diagram 5

Is this what you're asking?

untitled diagram 4

@rafaelfranca
Copy link
Member

Yes, exactly that.

@rafaelfranca
Copy link
Member

Sorry, hit the button really fast. I just think that maybe that is not something that the framework should generate by default. It is too messy in my opinion to have all that inheritance.

@dhh @matthewd options about this one?

@matthewd
Copy link
Member

matthewd commented Mar 1, 2017

I agree this seems very messy.

@gmalette could you show some examples of the monkey-patching this aims to help / alleviate?

@gmalette
Copy link
Contributor Author

gmalette commented Mar 1, 2017

@matthewd in our codebase we gradually, throughout the years, added setup on ActiveSupport::TestCase that does a bunch of things like:

  • deleting a Glob on the FS
  • loading data from S3 (memoized, but still needs to be done once)
  • flush memcached
  • flush redis
  • inject fixtures into redis
  • reset elasticsearch

This is fine for controller tests, elastic model tests, etc. However, we have a bunch of objects that don't need access to any form of database. All this setup is not useful to them.

Some people had used tests that extended Minitest::TestCase to get around this setup cost, but they've been removed for reasons @rafaelfranca can explain better me.

Bottom line is, for ~50% (guesstimate) of our tests, we want the costly setup. For the rest, we don't, and we also don't want to use Minitest::TestCase. Ideally these would be replaced by tests inheriting a ActiveSupport::TestCase that's not globally monkey-patched

edit last sentence

@gmalette
Copy link
Contributor Author

gmalette commented Mar 1, 2017

To be fair, the simplest way to solve our problem would be to add a layer between unit tests and ActiveSupport::TestCase (all other tests already have another inheritance). We could then monkey-patch that layer instead of ActiveSupport::TestCase itself. I just don't think it's good practice to patch stuff provided by the framework

@matthewd
Copy link
Member

matthewd commented Mar 1, 2017

And how would this PR, in its current form, help you there?

@gmalette
Copy link
Contributor Author

gmalette commented Mar 1, 2017

I'd be able to shove all the setup in ApplicationTestCase. We'd be able to have a light class class DBLessTestCase < ActiveSupport::TestCase that doesn't have this setup.

@dhh
Copy link
Member

dhh commented Mar 2, 2017

We have this exact problem in Basecamp as well. Here's what we currently do in test_helper.rb:

class ActiveSupport::TestCase
  include SignalId::Testing
  include ActiveJob::TestHelper, ActionMailer::TestHelper, ActionCableTestHelper,
          AttachmentTestHelper, BlobPreviewTestHelper, ContentTestHelper, ChatTestHelper, EmailTestHelper, IdentityTestHelper,
          NotificationsTestHelper, ReadingsTestHelper, ReportTestHelper, ScheduledJobTestHelper, TimeTestHelper, UserTestHelper,
          AccountTestHelper

  set_fixture_class playground_prefabs: Playground::Prefab
  fixtures :all

  setup    { FileUtils.mkdir_p BC3::Application.config.x.local_storage.root }
  teardown { FileUtils.rm_rf   BC3::Application.config.x.local_storage.root }

  setup    { FileRepository::Cleversafe::FakeServer.setup! }

  teardown { travel_back }
  teardown { Current.reset }

  teardown { RedisConnectable.clear_all }
  setup    { ApplicationJob.skip_reset_current = true }
  teardown { ActionMailer::Base.deliveries.clear }
  teardown { Rails.cache.clear }
end

class ActionController::TestCase
  include SessionTestHelper, CookieTestHelper

  teardown { Time.zone = "UTC" }

  setup do
    @request.host = accounts('37s').domain
    @request.env['bc3.account.queenbee_id'] = accounts('37s').queenbee_id
  end
end

class ActionDispatch::IntegrationTest
  include ApiTestHelper, SignalId::Testing, SessionIntegrationTestHelper

  setup do
    host! BC3.domain.hostname

    # Nullify the reset so a session can carry on between requests
    Current.singleton_class.send(:alias_method, :real_reset, :reset)
    Current.singleton_class.send(:define_method, :fake_reset) { }
    Current.singleton_class.send(:alias_method, :reset, :fake_reset)

    WebMock.disable_net_connect!(allow_localhost: true)
    WebMock.stub_request(:any, /#{Regexp.escape(Launchpad.url)}/).to_rack(Launchpup.new(Rails.logger))
  end

  teardown do
    Current.singleton_class.send(:alias_method, :reset, :real_reset)
  end

  def page
    Capybara::Node::Simple.new(@response.body)
  end
end

It'd be much better if that configuration happened in ApplicationTestCase and friends. So I'd definitely support this. Any time we're doing mixins and changes like this, it should happen in a Application* class rather than straight on the framework classes.

@gmalette
Copy link
Contributor Author

gmalette commented Mar 2, 2017

Here would be my revised plan: change the generators, to introduce ApplicationModelTestCase, ApplicationJobTestCase, ApplicationControllerTestCase, etc. Applications will be free to have a broad-painting application-level mixin (ApplicationTestCase in my diagram) they use in all of these, but not opinionated by Rails.

Ultimately this will introduce a layer between the test cases and Rails.

Good to go?

Also any thought about how to handle the "upgrade"? Will we need to generate those classes in applications? I think yes. I don't think it would be a breaking change, but how resistant will people be to this refactor?

edit: class names

@dhh
Copy link
Member

dhh commented Mar 2, 2017 via email

@gmalette
Copy link
Contributor Author

gmalette commented Mar 2, 2017

So I still see ModelTestCase < ApplicationTestCase

😕 I'm not sure how to do that... ? Can we really have both ApplicationControllerTestCase < ApplicationTestCase AND ApplicationJobTestCase < ApplicationTestCase while preserving backward compatibility?

I guess we could move the whole content of all current base test classes to mixins, such that ActionController::TestCase includes ActionController::TestCaseConcern, then generate as

ApplicationControllerTestCase < ApplicationTestCase
  includes ActionController::TestCaseConcern

untitled diagram 7

I'm a bit unconvinced by the use of mixins in application level code, but if you think it's better I don't mind. Unless that's not what you meant and/or have better ideas?

@dhh
Copy link
Member

dhh commented Mar 2, 2017 via email

@gmalette
Copy link
Contributor Author

gmalette commented Mar 2, 2017

Do you dislike specializing all the base classes for the application and let applications decide whether they need shared concerns? Then they can use ApplicationTestConcern and mix it in all the application classes.

class ApplicationMailerTestCase < ActionMailer::TestCase
  include ApplicationTestConcern

@dhh
Copy link
Member

dhh commented Mar 2, 2017 via email

@kaspth
Copy link
Contributor

kaspth commented Mar 6, 2017

@dhh iirc most of the test cases have a behavior module that they mixin, so they already are concerns.

How do we make sure that the behavior in ApplicationTestCase goes into ActionController::TestCase etc. as well? Since that's currently what happens with plain old class ActiveSupport::TestCase.

@dhh
Copy link
Member

dhh commented Mar 10, 2017

Hmm, yeah, I think we're going to get a timing issue if we just had a specially named concern that the framework then knew to include. Perhaps there isn't an easy fix here.

@kirs
Copy link
Member

kirs commented Mar 10, 2017

@dhh I see you're using fixtures :all in Basecamp test_helper. Did you encounter any performance issues related to inserting all fixtures when running the tests locally?

At Shopify, we're using fixtures :all too but now it became a huge pain point because it has to insert 500 fixture sets (that's what we have so far) to run a single test that maybe doesn't even make any database calls. We've been trying to figure out some kind of annotations for every test case whether they rely on persistence layer or not. Right now it may take 11 seconds for a complete run of test/unit/some_pure_ruby_class_test.rb, with 10 sec to insert fixtures and 1 sec to actually run the test.

@gmalette
Copy link
Contributor Author

gmalette commented May 2, 2018

I'll close this as we couldn't find a good way to do this. I'll be more than happy to pick it up if anyone has suggestions that solves all the concerns.

@gmalette gmalette closed this May 2, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants