Rails fragment cache testing with RSpec - ruby-on-rails

I feel like this is a not-so-much documented topic, at least I've had a lot of trouble finding our about the best practices here.
I'm fragment caching in the view using a cache_key:
%tbody
- #employees.each do |employee|
- cache employee do
%tr[employee]
%td= employee.name
%td= employee.current_positions
%td= employee.home_base
%td= employee.job_classes
Now I can add :touch => true on the :belongs_to side of my has_many associations and this will do everything I need to keep this fragment caching up to date, but for the life of me I'm having a hard time figuring out how to test this.
Dropping in :touch => true is easy and convenient but it spreads the expiry logic around a couple places. I'd love to have an RSpec request spec that walks through and checks the behavior on this, something that isn't liable to change much but can bring all the caching requirements into one specific file that describes what is supposed to be occurring.
I tried along these lines:
require 'spec_helper'
include AuthenticationMacros
describe "Employee index caching" do
before do
Rails.cache.clear
ActionController::Base.perform_caching = true
login_confirmed_employee
end
after do
ActionController::Base.perform_caching = false
end
specify "the employee cache is cleared when position assignments are modified"
specify "the employee cache is cleared when home base assignments are modified"
end
The specs were fleshed out with the Capybara steps of going through and making the updates of course, and I thought I was quite on the right track. But the tests were flickering in weird ways. I would modify the specs to output the employee objects cache_key, and sometimes the cache_keys would change and sometimes not, sometimes the specs would pass and sometimes not.
Is this even a good approach?
I know SO wants questions that are answerable, so to start: how can I set up and tear down this test to use caching, when my test env does not have caching on by default? In general, however, I'd really like to hear how you might be successfully testing fragment caching in your apps if you have had success with this.
EDIT
I'm accepting cailinanne's answer as it addresses the problem that I specifically ask about, but I have decided however that I don't even recommend integration testing caching if you can get away from it.
Instead of specifying touch in my association declarations, I've created an observer specific to my caching needs that touches models directly, and am testing it in isolation.
I'd recommend if testing a mulit-model observer in isolation to also include a test to check the observers observed_models, otherwise you can stub out too much of reality.
The particular answer that lead me to this is here: https://stackoverflow.com/a/33869/717365

Let me first say that in this answer, you may get more sympathy then fact. I've been struggling with these same issues. While I was able to get reproducible results for a particular test, I found that the results varied according to whether or not I ran one versus multiple specs, and within or without spork. Sigh.
In the end, I found that 99.9% of my issues disappeared if I simply enabled caching in my test.rb file. That might sound odd, but after some thought it was "correct" for my application. The great majority of my tests are not at the view/request layer, and for the few that are, doesn't it make sense to test under the same configurations that the user views?
While I was wrestling with this, I wrote a blog post that contains some useful test helpers for testing caching. You might find it useful.

Here is what I've used in my specs with caching enabled in my config/environments/test.rb
require 'spec_helper'
include ActionController::Caching::Fragments
describe 'something/index.html.erb' do
before(:each) do
Rails.cache.clear
render
end
it 'should cache my fragment example' do
cached_fragment = Rails.cache.read(fragment_cache_key(['x', 'y', 'z']))
cached_fragment.should have_selector("h1")
end
end

I use view specs to test cache expiration in views:
describe Expire::Employee, type: :view, caching: true do
def hour_ago
Timecop.travel(1.hour.ago) { yield }
end
def text_of(css, _in:)
Nokogiri::HTML(_in).css(css).text
end
let(:employee) { hour_ago { create :employee } }
def render_employees
assign(:employees, [employee.reload])
render(template: 'employees/index.html.erb')
end
alias_method :employees_list, :render_employees
context "when an employee's position gets changed" do
let(:position) { create :position, employee: employee }
before { hour_ago { position.update!(name: 'Old name') } }
let(:update_position) { position.update!(name: 'New name') }
it "should expire the employee's cache" do
expect { update_position }
.to change { text_of('.positions', _in: employees_list) }
.from(/Old name/).to(/New name/)
end
end
# similar spec case for home base assignment
end
where
Timecop gem is used to travel in time to make sure that cache key timestamps are different for different cache versions of employee
Nokogiri gem is used to extract employee position's text from the rendered view
Note that I tagged this spec with caching: true. It enables caching before each test case and disables after it:
config.before(:each, caching: true) do
controller.perform_caching = true
end
config.after(:each, caching: true) do
controller.perform_caching = false
end
And you might want to add an example that checks that an employee is being actually cached
describe Expire::Employee, type: :view, caching: true do
context 'with an uncached employee' do
it 'should cache the employee' do
expect_any_instance_of(Employee)
.to receive(:current_positions).once
2.times { render_employees }
end
end
# other spec cases
end

Related

Should we test rails attributes?

In Rails models we usually have attributes and relations tests, like:
describe 'attributes' do
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
end
describe 'relations' do
it { is_expected.to belong_to(:user).class_name('User') }
end
And using a TDD style it seems to be some useful tests, however I have been dwelling if these are really necessary tests, and I would like to know if there is some common knowledge about it, is it good practice to create these tests? or are we just testing rails?
Amongst the purposes of a unit test are...
Does it work?
Does it still work?
If it's a promise, if other things rely on it, you should test it to ensure you keep that promise. This is regression testing.
But don't test more than you promise. You'll be stuck with it, or your code will break when you make an internal change.
For example...
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
This promises that it has a column called identifier which is a UUID. Usually you don't promise all that detail; it is glass-box testing and it makes your test brittle.
Instead, promise as little as you can. Its ID is a UUID. This is black-box testing.
require "rspec/uuid"
describe '#id' do
subject { thing.id }
let(:thing) { create(:thing) }
it 'has a uuid ID' do
expect(thing.id).to be_a_uuid
end
end
It's possible there is an even higher level way to express this without holding yourself specifically to a UUID.
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
Similarly, don't promise it has a jsonb column. That is blackbox testing. Promise that you can store complex data structures.
describe '#content' do
subject { create(:thing) }
it 'can round trip complex data' do
data = [1, { two: 3, four: [5] }]
thing.update!(content: data)
# Force it to re-load content from the database.
thing.reload
expect(thing.content).to eq data
end
end
it { is_expected.to belong_to(:user).class_name('User') }
Instead of promising what it belongs to, promise the relationship.
describe '#user' do
let(:thing) { create(:thing) }
let(:user) { create(:user) }
before {
user.things << thing
}
it 'belongs to a user' do
expect(thing.user).to eq user
expect(user.things).to contain(thing)
end
end
I have answered a nearly identical question here: https://stackoverflow.com/a/74195850/14837782
In summary: If it is end-developer code, I believe it should be tested. If it can be fat-fingered, I believe it should be tested. If you're going to remove it deliberately, I also believe you should have to remove a test deliberately as well. If it can fail, there should be a specific test for that failure mode.
This is not to be confused with testing the Rails framework. You obviously want to design your tests so that you're not testing Rails itself or Rails implementation, only your own code.
Attributes should be tested. Here is how I do it in minitest:
test/models/car_test.rb
class CarTest < ActiveSupport::TestCase
###################################################################
#
# Attributes
#
###################################################################
test 'describe some attr_reader fields' do
expected = [:year, :make, :model, :vin]
assert_has_attr_readers(Car, expected)
end
###############################################
test 'describe some attr_writer fields' do
expected = [:infotainment_fimrware_version]
assert_has_attr_writers(Car, expected)
end
###############################################
test 'describe some attr_accessor fields' do
expected = [:owner, :color, :mileage]
assert_has_attr_readers(Car, expected)
assert_has_attr_writers(Car, expected)
end
end
test/test_helpers/attributes_helper.rb
# frozen_string_literal: true
module AttributesHelper
###################################################################
#
# Assertions
#
###################################################################
#
# Performs an assertion that the given class contains reader/getter methods for the given attribute names.
# This helper checks for the existence of `attribute_name` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_reader, attr_accessor, etc.
#
def assert_has_attr_readers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a reader for attribute #{attribute}"
assert_includes(actual_method_names, attribute.to_s, message)
end
end
#
# Performs an assertion that the given class contains writer/setter methods for the given attribute names.
# This helper checks for the existence of `attribute_name=` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_writer, attr_accessor, etc.
#
def assert_has_attr_writers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a writer for attribute #{attribute}"
assert_includes(actual_method_names, "#{attribute}=", message)
end
end
#
# Performs an assertion that the given class implements attr_encrypted for the given attribute names.
# This helper is tied to the implementation details of the attr_encrypted gem. Changes to how attributes
# are encrypted will need to be accounted for here.
#
def assert_has_encrypted_attrs(klass, attribute_names)
message = "Expected class #{klass.name} to encrypt specific attributes"
actual_attributes = klass.encrypted_attributes.keys
assert_equal(attribute_names.map(&:to_s).sort, actual_attributes.map(&:to_s).sort, message)
end
end
Your example tests seem to be testing the existence of DB fields, not getter/setter model attributes. Database fields are impossible to fat-finger (they require a migration to modify) so if that's what you're talking about, I do not believe it makes sense to test them. (And I personally believe it is useful to test nearly everything.)
Although I guess in the case where the DB is accessible by other applications and could potentially be modified outside of a single application then it could make sense to test for the existence of those fields as well, as pointed out by Dave Newton in a comment below.
Ultimately it is up to you, and if your one application is the only one with access to the DB but you still want to test DB field existence and settings, maybe a 3rd option is some sort of migration test that you're looking for to make sure the migration is written properly. I've not written anything like that yet, but it might be feasible. I would hate to try to write one, and it does seem to go too far, but it's an idea...

rspec if Rails.env.production? return string how to implemented it?

And it does not work. Can you help me how to write the right test for its case? Thanks very much.
#model.rb
def driver_iq_api
if Rails.env.production?
'https://admin.sss/xmlpost.cfm'
else
'https://eeem/ws/xmlpost.cfm'
end
end
model_spec.rb
describe 'private methods' do
context '.driver_iq_api' do
it 'production true' do
allow(Rails.env).to receive(:production?) {true}.and_return('https://admin.sss/xmlpost.cfm')
end
it 'production false' do
allow(Rails.env).to receive(:production?) {false}.and_return('https://eeem/ws/xmlpost.cfm')
end
end
end
Setting Rails.env to something other than test, inside a test, is a bad idea. Whilst you may "get away with it" in this case, it could cause all sorts of weird side-effects in general, such as writing data to a non-test database.
In addition, it seems you're writing unit tests for private methods, which is typically a bad idea. You should only normally test the public interface of a class.
As stated above, this sort of config should ideally live in a configuration file, such as e.g. application.yml.
The other answer already shows how you could stub the behaviour, but as yet another alternative, you could consider injecting the environment as a method dependency:
def driver_iq_api(env: Rails.env)
if env.production?
'https://admin.sss/xmlpost.cfm'
else
'https://eeem/ws/xmlpost.cfm'
end
end
describe '#driver_iq_api' do
it 'production env' do
expect(model.driver_iq_api(env: 'production'.inquiry)).to eq 'https://admin.sss/xmlpost.cfm'
end
it 'test env' do
expect(model.driver_iq_api(env: 'test'.inquiry)).to eq 'https://eeem/ws/xmlpost.cfm'
end
end
Note that for example, 'test'.inquiry returns a ActiveSupport::StringInquirer instance - which is the same behaviour as calling Rails.env.
...But to reiterate my original point, I wouldn't bother testing this method at all.
As for you test, I'm not familiar with the receive...{block} syntax you use, and I doubt seriously that tests what you think it is testing.
Here is a test suite that tests much more succinctly, and is super easy for anyone to read:
describe '.driver_iq_api' do
subject { MyModel.driver_iq_api }
context 'when in production' do
allow(Rails.env).to receive(:production?).and_return(true)
it { is_expected.to eq 'https://admin.sss/xmlpost.cfm' }
end
context 'when not in production' do
it { is_expected.to eq 'https://eeem/ws/xmlpost.cfm' }
end
end
And to reinforce what #Tom Lord said, this approach is dangerous for methods that actually do things like write to databases and such. I'd use this only for the type of method you have in your example...returning a resource name, boolean, etc. based on env. If your platform-sensitive code is buried deep in a method and can't be tested in isolation, then refactor it out of the big method into an atomic method that can easily be tested (and mocked!).
I agree you should pull this out into config since it is static, but to answer your question:
it "when production" do
allow(Rails.env).to receive(:production?).and_return(true)
expect(my_class.send(:driver_iq_api)).to eq('https://admin.sss/xmlpost.cfm')
end

How to test correlated (coupled) methods in RSpec?

Let's say we have class:
class Post
def save
# implementation
end
def self.find(id)
#implementation
end
end
I struggle with testing #save and .find, I've:
describe '#save' do
it 'saves the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
describe '.find' do
it 'finds the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
In case of #save method I'd like to check side effect, in case of .find I'd like to test returned value. How to cope with this case without duplicating specs ?
In this case, to isolate the save and find actions, you need to mock the repository.
Whether you are writing to a DB, a file-system, cache, or whatever - you can mock it to either expect the saving feature, or set it up (before the beginning of the test) to make sure find works.
For most repository implementations there are gems to mock them (Factory Girl for relational databases, FakeFS for file-system), but you can roll your own if you have some exotic repository no one has heard of.
This way you test save without using find, or vice versa.

Elasticsearch out of sync when overwhelmed on HTTP at test suite

I have a Rails app with an Rspec test suite which has some feature/controller tests depending on ElasticSearch.
When we test the "search" feature around the system (and other features depending on ES) we use a real ES, it works perfectly at development environment when we're running single spec files.
When the suite runs at our CI server it gets weird, because sometimes ES won't keep in sync fast enough for the tests to run successfully.
I have searched for some way to run ES in "syncronous mode", or to wait until ES is ready but haven't found anything so far. I've seen some workarounds using Ruby sleep but it feels unacceptable to me.
How can I guarantee ES synchronicity to run my tests?
How do you deal with ES on your test suite?
Here's one of my tests:
context "given params page or per_page is set", :elasticsearch do
let(:params) { {query: "Resultados", page: 1, per_page: 2} }
before(:each) do
3.times do |n|
Factory(:company, account: user.account, name: "Resultados Digitais #{n}")
end
sync_companies_index # this is a helper method available to all specs
end
it "paginates the results properly" do
get :index, params
expect(assigns[:companies].length).to eq 2
end
end
Here's my RSpec configure block and ES helper methods:
RSpec.configure do |config|
config.around :each do |example|
if example.metadata[:elasticsearch]
Lead.tire.index.delete # delete the index for a clean environment
Company.tire.index.delete # delete the index for a clean environment
example.run
else
FakeWeb.register_uri :any, %r(#{Tire::Configuration.url}), body: '{}'
example.run
FakeWeb.clean_registry
end
end
end
def sync_companies_index
sync_index_of Company
end
def sync_leads_index
sync_index_of Lead
end
def sync_index_of(klass)
mapping = MultiJson.encode(klass.tire.mapping_to_hash, :pretty => Tire::Configuration.pretty)
klass.tire.index.create(:mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings)
"#{klass}::#{klass}Index".constantize.rebuild_index
klass.index.refresh
end
Thanks for any help!
Your test is confused - it's testing assignment, pagination, and (implicitly) parameter passing. Break it out:
Parameters
let(:tire) { double('tire', :search => :sentinel) }
it 'passes the correct parameters to Companies.tire.search' do
expected_params = ... # Some transformation, if any, of params
Companies.stub(:tire).with(tire)
get :index, params
expect(tire).to have_received(:search).with(expected_params)
end
Assignment
We are only concerned that the code is taking one value and assigning it to something else, the value is irrelevant.
it 'assigns the search results to companies' do
Companies.stub(:tire).with(tire)
get :index, params
expect(assigns[:companies]).to eq :sentinel
end
Pagination
This is the tricky bit. You don't own the ES API, so you shouldn't stub it, but you also can't use a live instance of ES because you can't trust it to be reliable in all testing scenarios, it's just an HTTP API after all (this is the fundamental issue you're having). Gary Bernhardt tackled this issue in one of his excellent screencasts - you simply have to fake out the HTTP calls. Using VCR:
VCR.use_cassette :tire_companies_search do
get :index, params
search_result_length = assigns[:companies].length
expect(search_result_length).to eq 2
end
Run this once successfully then forever more use the cassette (which is simply a YAML file of the response). Your tests are no longer dependent on APIs you don't control. If ES or your pagination gem update their code, simply re-record the cassette when you know the API is up and working. There really isn't any other option without making your tests extremely brittle or stubbing things you shouldn't stub.
Note that although we have stubbed tire above - and we don't own it - it's ok in these cases because the return values are entirely irrelevant to the test.

How can I set up RSpec for performance testing 'on the side'

We are using RSpec in a rails project for unit testing. I would like to set up some performance tests in RSpec, but do it in a way as to not disrupt the 'regular' features and fixtures.
Ideally I'd be able to tag my performance specs in a certain way such that they are not run by default.
Then when I specify to run these specs explicitly it will load a different set of fixtures (it makes sense to do performance testing with a much larger and more 'production-like' dataset).
Is this possible? It seems like it should be.
Has anyone set up something like this? How did you go about it?
I managed to get what I was looking for via the following:
# Exclude :performance tagged specs by default
config.filter_run_excluding :performance => true
# When we're running a performance test load the test fixures:
config.before(:all, :performance => true) do
# load performance fixtures
require 'active_record/fixtures'
ActiveRecord::Fixtures.reset_cache
ActiveRecord::Fixtures.create_fixtures('spec/perf_fixtures', File.basename("products.yml", '.*'))
ActiveRecord::Fixtures.create_fixtures('spec/perf_fixtures', File.basename("ingredients.yml", '.*'))
end
# define an rspec helper for takes_less_than
require 'benchmark'
RSpec::Matchers.define :take_less_than do |n|
chain :seconds do; end
match do |block|
#elapsed = Benchmark.realtime do
block.call
end
#elapsed <= n
end
end
# example of a performance test
describe Api::ProductsController, "API Products controller", :performance do
it "should fetch all the products reasonably quickly" do
expect do
get :index, :format => :json
end.to take_less_than(60).seconds
end
end
But I tend to agree with Marnen's point that this isn't really the best idea for performance testing.
I created rspec-benchmark Ruby gem for writing performance tests in RSpec. It has many expectations for testing speed, resources usage, and scalability.
For example, to test how fast your code is:
expect { ... }.to perform_under(60).ms
Or to compare with another implementation:
expect { ... }.to perform_faster_than { ... }.at_least(5).times
Or to test computational complexity:
expect { ... }.to perform_logarithmic.in_range(8, 100_000)
Or to see how many objects get allocated:
expect {
_a = [Object.new]
_b = {Object.new => 'foo'}
}.to perform_allocation({Array => 1, Object => 2}).objects
If you want to do performance testing, why not run New Relic or something with a snapshot of production data? You don't really need different specs for that, I think.

Resources