Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 8, 2022 06:36 pm GMT

Testing scopes with Rails

The Problem

A common problem I've seen and that took me a long time to understand was how to properly test Rails scopes. I have searched online many times for how to properly tests scopes and find a bunch of old stackoverflow posts which really don't address the issue.

Why are they hard to test?

Scopes can be hard to test because generally you're operating on an entire database which if you include a large number of fixtures can cause global data to leak into your test and cause tests to fail intermittently.

The naive approach

Initially, I thought to "stub" out the ActiveRecord::Relation that scopes tend to operate on, but I found stubbing out the data to be error prone, rigid, and hard to get right.

The approach that operates on a known dataset

After fighting with this for so long, I finally came to a realization. Attach the scope to a known dataset and use that for the test case! But how do we do this? The easiest way is to create a set of records, and then pass their id into a where() query on the model. Let's look at an example.

Let's say we have a model called User and we want to have a scope for both newest and oldest users.

If you're like me, you never know the difference between ASC and DESC for datetime columns.

So lets add our scopes quick and then see if we get it right:

class User < ActiveRecord::Base  scope :newest, -> { order(created_at: :asc) }  scope :oldest, -> { order(created_at: :desc) }end

Now, how could we test it?

Well, in our model test we should first setup a few users.

class UserTest < ActiveSupport::TestCase  def setup    @user_one = User.create!(created_at: 1.day.ago)    @user_two = User.create!(created_at: 2.days.ago)    @user_three = User.create!(created_at: 3.days.ago)    # This lets us have a known set of data. We don't have a polluted global scope of users.    @users = User.where(id: [@user_one.id, @user_two.id, @user_three.id])  endend

Now that we have a known set of data, we chain off of it and test our scope.

class UserTest < ActiveSupport::TestCase  def setup    # ...  end  test "Newest users and oldest users should be sorted properly" do    # By testing both newest and oldest, we're coupling these tests together    # We test both just in case our records just happen to be returned in the correct order.    # we could use reverse_scope to test newest, but instead we just make this explicit.    # chain off of @users so we use a known set of data rather than the whole database.    newest_users = @users.newest    assert_operator newest_users.first.created_at, :>=, newest_users.second.created_at    assert_operator newest_users.first.created_at, :>=, newest_users.third.created_at    assert_operator newest_users.second.created_at, :>=, newest_users.third.created_at    oldest_users = @users.oldest    assert_operator oldest_users.first.created_at, :<=, oldest_users.second.created_at    assert_operator oldest_users.first.created_at, :<=, oldest_users.third.created_at    assert_operator oldest_users.second.created_at, :<=, oldest_users.third.created_at  endend

On running this test you should get something like this:

ruby user_test.rb# Running:FFinished in 0.013149s, 76.0489 runs/s, 76.0489 assertions/s.  1) Failure:UserTest#test_Newest_users_and_oldest_users_should_be_sorted_properly [single_line_active_record.rb:52]:Expected 2022-04-05 18:15:24.185615 UTC to be >= 2022-04-06 18:15:24.184933 UTC.

Oops! It failed...perhaps we got the column direction wrong? Let's try this:

class User < ActiveRecord::Base-  scope :newest, -> { order(created_at: :asc) }+  scope :newest, -> { order(created_at: :desc) }-  scope :oldest, -> { order(created_at: :desc) }+  scope :oldest, -> { order(created_at: :asc) }end

Then test it again:

ruby user_test.rb# Running:.Finished in 0.021307s, 46.9331 runs/s, 281.5983 assertions/s.1 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Yayyy we did it!!

Adding another scope

Alright, the first scope test may not have been too useful for this limited scope, so lets make an exclusionary scope that checks for users created after a certain date / time.

The scope

class User < ApplicationRecord  scope :created_after, ->(date) { where("created_at >= ?", date) }end

The test

class UserTest < ActiveSupport::TestCase  test "Should only show users created from 2 days ago and later" do    # Always use beginning of day. I believe 2.days.ago drops the "time" causing it to act like    # #end_of_day which will cause our user created "2.days.ago" to be excluded.    users = @users.created_after(2.days.ago.beginning_of_day)    # We know there should only be 2 users created within the last 2 days.    assert_equal users.size, 2    assert_includes users, @user_one    assert_includes users, @user_two  endend

Touchdown

$ ruby user_test.rb# Running:..Finished in 0.024298s, 82.3114 runs/s, 452.7127 assertions/s.2 runs, 11 assertions, 0 failures, 0 errors, 0 skips

Wrapping up

Now is this the first use case of checking created_at useful for this type of limited scope testing? Maybe not. For this particular test of testing creation time, it shouldn't matter because we expect the sorting algorithm to get it right. However, using this method of a limited scope of data makes it easier to start testing more advanced scopes and scopes where data from other tests / fixtures may leak in and its much easier to test results. It also works great on exlusionary scopes / queries like we did with our created_after scope.

Recreation

A gist can be found here of recreating this testing in a single file:

https://gist.github.com/ParamagicDev/f59d02bed10308c9ca60a43c87de26d9


Original Link: https://dev.to/paramagicdev/testing-scopes-with-rails-4ho9

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To