> Every tiny rails app that uses rspec ends up with +1hr test times due to tight coupling between activemodel and the db connections.
Wat? I mean test times aren't great but I've got several thousand specs for what I would call a medium-sized application that run in <1m using TurboTests or <3m using rspec alone. I can't imagine a "tiny" rails app that takes an hour to test. Maybe you're doing a lot of browser rendering or something? My app is API only.
I think the fundamental design problem for rspec is for each test you create db state (tons of writes), run the test (reads and writes), and then tear-down (truncate). Each test is at least adding an authorized user (and then removing it) at the simplest and at the worst creating complicated db state to support relational models that must be added and torn down with each test (database_cleaner).
Gems like Fabricator or FactoryBot also make the "create db state" even more excessive, because developers would be lazy and use factory methods that create more than they actually need for the test.
For example, one project I stored a tree-structure that could be modified by api calls. Each test required re-creating the 15 node tree + each node's relational data (think node = user and all users must have a profile, authorization scheme, etc.).
I never figured out how to avoid resetting the db state for each test case.
This isn't an rspec problem, it's a Factories-for-ActiveRecord problem.
The two main options to mitigate are:
- just work with in-memory objects (FactoryBot using build or build_stubbed, or just MyModel.new) and stub/mock finders as needed,
- or there's this thing people like to hate that was introduced in Rails 1 called test fixtures, which allow you to prepopulate the database with some initial data and then your test just runs in a transaction and rolls back.
The best suggestion I've heard recently (though I can't recall where to give credit) for managing complexity w/ fixtures is to write an extremely minimal fixture set aiming for 2 instances per model with minimal data required to be valid, and then customize it as part of your test. For a blog example, that means you have two Posts in your fixtures and would write a test around drafts by updating one of them to draft status (be it state, toggle, or publication date) and then test your query, or controller, or whatever. This keeps the data churn inside each test minimal, while keeping the out-of-scope data minimal as well -- if there's only 2 blog posts, all your tests can assume there will be one you care about and one you don't, which is useful for testing filtering/search/visibility/authorization/etc while remaining pretty consistent.
> - just work with in-memory objects (FactoryBot using build or build_stubbed, or just MyModel.new) and stub/mock finders as needed,
Mocking the finders is obnoxious, since ActiveRecord returns a relation class, not an array. Plus stubbing out relational coupling isn't easy (e.g. the user model might have a `company_name` method that delegates to the company table.).
> - or there's this thing people like to hate that was introduced in Rails 1 called test fixtures, which allow you to prepopulate the database with some initial data and then your test just runs in a transaction and rolls back.
Fixtures and Factories have the same issue are vulnerable to inserting unnecessary data for the test. Models may have validation requirements that must be satisfied to be stored, but completely unnecessary for the test. For example, all users need a `company_id` with a foreign key constraint, so to test _anything_ on user, you have to insert a valid company as well.
Maybe I need to re-evaluate fixtures though, since they would be simpler to run than a factory.
The idea is that you write one set of fixtures for the whole app that you then use in all tests. You'd have a valid company and a valid user in your fixtures, but that's fine because you probably want to test both the company and user models. In the user model tests, you can ignore the existence of the company fixtures, and vice versa.
Since the inserts only happen once for the whole test suite, the marginal cost of adding more fixtures is minimal, so it makes sense to just make the fixtures as complete as possible.
interesting. I haven't used fixtures before. Wouldn't this make individual tests slow (like in development?) since all fixtures must be inserted all the time?
It doesn't add as much overhead as you might expect because the fixture data is inserted into the database without instantiating any ActiveRecord models. Unless you're loading a truly crazy amount of fixtures (gigabytes?), the database can ingest all the data in single-digit milliseconds.
Wat? I mean test times aren't great but I've got several thousand specs for what I would call a medium-sized application that run in <1m using TurboTests or <3m using rspec alone. I can't imagine a "tiny" rails app that takes an hour to test. Maybe you're doing a lot of browser rendering or something? My app is API only.