What's the benefit of Class.new in this Rspec - ruby-on-rails

I am reading through some Rspec written by someone who left the company. I am wondering about this line:
let(:mailer_class) { Class.new(AxeMailer) }
let(:mailer) { mailer_class.new }
describe '#check' do
before do
mailer_class.username 'username'
mailer.from 'tester#example.com'
mailer.subject 'subject'
end
subject { lambda { mailer.send(:check) } }
It is testing this class:
class AxeMailer < AbstractController::Base
def self.controller_path
#controller_path ||= name.sub(/Mailer$/, '').underscore
end
I want to know the difference between this and let(:mailer_class) { AxeMailer }.
I ask this because currently when I run the test, it will complain name is nil. But if I changed it, it will test fine.
I think this issue started after using Rails 3.2, and I think name is inherited from AbstractController::Base.
This is the same in the console (meaning it is not Rspec specific), I can do AxeMailer.name with no error, but if I do Class.new(AxeMailer) there is is the problem.
My questions are:
Is there a reason to use Class.new(AxeMailer) over AxeMailer
Is there a problem if I just change this?
Is there a way not change the spec and make it pass?

I'm guessing it was written this was because of the mailer_class.username 'username' line. If you just used AxeMailer directly, the username setting would be carried over between tests. By creating a new subclass for each test, you can make sure that no state is carried over between them.

I don't know if mailer_class is being used inside the actual spec or not, but this is what I think your setup should look like:
let(:mailer) { AxeMailer.new }
describe '#check' do
before do
AxeMailer.username 'username'
mailer.from 'tester#example.com'
mailer.subject 'subject'
end
subject { lambda { mailer.send(:check) } }
There just doesn't seem to be a need for the anonymous class that was being created. Also, this is just my opinion, but your subject looks a bit odd. Your spec should probably wrap the subject in a lambda if it needs to, but don't do that in your subject.
Regarding the error you were seeing originally, anonymous classes don't have names:
1.9.3-p0 :001 > Class.new.name
=> nil
Some part of ActionMailer::Base must attempt to use the class name for something (logging perhaps) and breaks when it's nil.

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...

How to test/mock Trailblazer operations that interact with external gems?

I'm loving all the object oriented beauty of Trailblazer!
I have an operation that interacts with a gem (called cpanel_deployer) to do something externally on the web. (It adds an addon domain to a cpanel.)
class Website::Deploy < Trailblazer::Operation
attr_reader :website, :cpanel
def process(params)
real_cpanel_add_domain
website.cpanel = cpanel
website.save
end
private
def setup!(params)
#cpanel = Cpanel.find(params[:cpanel_id])
#website = website.find(params[:website_id])
end
def real_cpanel_add_domain
cp_domain = CpanelDeployer::Domain.new(website.domain)
cp_panel = CpanelDeployer::Panel.new(cpanel.host, cpanel.username, cpanel.password)
res = cp_panel.add_domain(cp_domain)
raise StandardError unless res
end
end
The cpanel_deloyer gem is already tested, so I don't need to retest it's functionality here. But in order to test the operation, I want to make sure CpanelDeployer::Panel#add_domain is called with correct args. So I'm thinking I should mock CpanelDeployer::Panel.
I believe it's bad practice to try to use any_instance_of. According to thoughtbot, it's usually considered code smell... They recommend using dependency injection. Is there a good way to use dependency injection within a trailblazer operation? Is there another best practice for this kind of situation?
One option is to stub :new on the gem's classes and return test doubles. Here's what that looks like:
describe Website::Deploy do
let(:cpanel) { Cpanel::Create.(cpanel: {
host: 'cpanel-domain.com', username: 'user', password: 'pass'
}).model }
let(:website) { Website::Create.(website: { domain: 'domain.com' }).model }
it 'works' do
fake_cp_domain = double(CpanelDeployer::Domain)
fake_cp = double(CpanelDeployer::Panel)
expect(fake_cp).to receive(:add_domain).with(fake_cp_domain).and_return(true)
expect(CpanelDeployer::Domain).to receive(:new)
.with(website.domain)
.and_return(fake_cp_domain)
expect(CpanelDeployer::Panel).to receive(:new)
.with(cpanel.host, cpanel.username, cpanel.password)
.and_return(fake_cp)
Website::Deploy.(cpanel_id: cpanel.id, website_id: website.id)
end
end
This seems pretty cumbersome... Is there a better way?
Honestly, I don't really understand what real_cpanel_add_domain is doing, because it seems to me that it just assigns two local variables and then calls add_domain on one of them, how would that affect anything?
Speaking about dependency injection, I guess you can get domain and panel classes from params, defaulting to CpanelDeployer::Domain and CpanelDeployer::Panel, but passing some stubs in your specs.
I'm not a big fan of stubbing new method, because it not always works as expected.

How to test model method with new rspec syntax?

In the below spec, I want to test that for a Notification instance, running the destructive method #mark_as_dismissed will change the has_been_read column to true. How do you do that while keeping the test nice and terse?
context "#mark_as_dismissed" do
subject { create(:notification) }
subject.mark_as_dismissed # How do I specify this
its(:has_been_read) { should be_true }
end
There's multiple ways to write tests and different preferred syntaxes, so this is quite an opinionated answer.
Following your style, it would look more like this:
describe "#mark_as_dismissed" do
subject { create(:notification) }
before { subject.mark_as_dissmissed }
its(:has_been_read) { should be_true }
end
Mine would be more like this:
describe "#mark_as_dismissed" do
let(:notification) { create(:notification) }
it "marks the notification as read" do
notification.mark_as_dissmissed
notification.has_been_read.should be_true
end
end
syntatic sugar: Rspec allows a special syntax for methods returning booleans. I would have to test it, I am not sure it would work in this case but perhaps you can do something like:
# for the first alternative
it { should have_been_read }
# for the second alternative
it "marks the notification as read" do
notification.mark_as_dissmissed
notification.should have_been_read
end
Bonus points
To remove the db dependency you can just 'build' the notification instead of using 'create' (which persists the model in the database). If the #mark_as_dismissed method does not need the db (can do a non persistent update), then the test should still work.
build(:notification) # instead of create(:notification)

Mocks and Stubs. I don't get the basics

I am in the process of freeing myself from FactoryGirl (at least in the lib folder). So, I start writing strange stuff like "mock" and "stub". Can somebody help a novice out?
I have this module
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
log and todo are rails models with a todo :has_many logs association. But that should really not matter when working with stubs and mocks, right?
I have tried many things, but when I pass the mock to the method nothing happens,
describe LogWorker do
it 'should check_todo_on_log'do
todo = mock("todo")
log = mock("log")
log.stub!(:todo).and_return(todo)
todo.stub!(:completed).and_return(false)
LogWorker.check_todo_on_log(log,1)
log.todo.completed.should eq true
end
end
Failures:
1) LogWorker should check_todo_on_log
Failure/Error: log.todo.completed.should eq true
expected: true
got: false
(compared using ==
I would really like to see some spec that would test the LogWorker.check_todo_on_log method with stubs and/or mocks.
Firstly, your check_todo_on_log method is pretty bad. Never, ever use strings as options, especially when the string is "1". Also, if you pass "2", nothing happens. I'll assume though it is just a partial method, and your code isn't really like that :P
Looking at your code, you have three main problems. Firstly, you call LogWorker.check_todo_on_log(log,1). This won't do anything, as your method only does stuff when the second param is the string "1" or nil. Secondly, you stub todo.completed so it always returns false: todo.stub!(:completed).and_return(false). You then test if it is true. Obviously this is going to fail. Finally, you don't mock the save! method. I don't know how the code is actually running for you (it doesn't work for me).
Below is how I would write your specs (note that they are testing weird behaviour as the check_todo_on_log method is also strange).
Firstly, there is an easier way to add mock methods to a mock object. You can pass keys and values to the mock methods, and they will automatically be created.
Next, I put the mocks into let blocks. This allows them to be recreated easily for each test. Finally, I add a test for each possible behaviour of the function.
# you won't need these two lines, they just let the code be run by itself
# without a rails app behind it. This is one of the powers of mocks,
# the Todo and Log classes aren't even defined anywhere, yet I can
# still test the `LogWorker` class!
require 'rspec'
require 'rspec/mocks/standalone'
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
describe LogWorker do
let(:todo) { mock("Todo", save!: true) }
let(:log) { mock("Log", todo: todo) }
describe :check_todo_on_log do
it 'checks todo when done is "1"'do
todo.should_receive(:completed=).with(true)
LogWorker.check_todo_on_log(log,"1")
end
it 'unchecks todo when done is nil'do
todo.should_receive(:completed=).with(false)
LogWorker.check_todo_on_log(log,nil)
end
it "doesn't do anything when done is not '1' or nil" do
todo.should_not_receive(:completed=)
LogWorker.check_todo_on_log(log,3)
end
end
end
Notice how I am using behaviour based testing? I'm not testing that an attribute on the mock has a value, I am checking that an appropriate methods are called on it. This is the key to correctly using mocks.

How to get instance_of to work with ActiveRecord objects in RoR?

Today I ran into an issue using RoR to stub calls to AR objects. I thought that I'd be able to do something along the lines of :
stub.instance_of(BankAccount).save_to_other_spot { true }
However when I tried this method it didn't seem to stub the method at all and it would end up running the original method I was trying to stub. I confirmed this using debugger etc.
So I ended up using the following method :
stub.proxy(BankAccount).find(anything) do |account|
stub(account).save_to_other_spot { true }
account
end
This works.
I was wondering if I'm doing something wrong though? Why doesn't instance_of work in the way I expect?
Another issue I ran into was that in my RSpec tests I seem to have to setup my mocks and stubs for each request. Again, is this normal or am I doing something wrong?
By this I mean I'd have to do something like :
... mock and stub ...
get :show, :id => #id
... mock and stub ...
post :update, :id => id, :account => { ... params ... }
I thought I'd be able to mock and stub once at the top.
Assuming you are running RSpec >2.5... then the mock/stubbing syntax has been improved so that you can now use the following definition.
BankAccount.any_instance.stub(:save_to_other_spot) { true }
Note you will need to use a latter version of RSpec. The earlier versions of RSpec do not include the any_instance method. It looks like they borrowed this from Mocha and implemented it into RSpec mocks.
If you are using the older versions of RSpec, then what you're doing I think is the only way. Only that I tend to write it like this:
#bank_account = BankAccount.new
BankAccount.stub(:find) { #bank_account }
#bank_account.stub(:save_to_other_spot) { true }
Albeit I think your block method looks cleaner.

Resources