I've got POST endpoint which creates a new JourneyProgress record in my db.
post :enroll do
JourneyProgress.create!(user: current_user, journey: journey, percent_progress: 0.0, started_at: DateTime.now)
status :no_content
end
I want to check if percent_progress and started_at were set with below example:
let(:current_date) { 'Thu, 16 Jul 2020 17:08:02 +0200'.to_date }
before do
allow(DateTime).to receive(:now) { current_date }
end
it 'set starting progress' do
call
expect(JourneyProgress.last.started_at).to eq(current_date)
expect(JourneyProgress.last.percent_progress).to eq(0.0)
end
The specs will pass but I'm not sure if JourneyProgress.last.(some record name) is in line with convention. Is there a better way to check this?
If I change it to:
it 'set starting progress' do
expect(call.started_at).to eq(current_date)
...
end
I'm getting an error:
NoMethodError:
undefined method `started_at' for 204:Integer
If you really want to test the value of the started_at column, something like this would work.
it 'set starting progress' do
call
expect(JourneyProgress.last.started_at).to eq(current_date)
end
but I'd suggest that you think twice about what is worth testing in this scenario, it would make a lot more sense to check that a JourneyProgress record is being inserted into your DB and that the endpoint actually returns the correct HTTP Status code.
it 'persists the record in database' do
expect { call }.to change { JourneyProgress.count }.by(1)
end
it 'responds with no content status' do
call
expect(response).to have_http_status(:no_content)
end
As other comments state I'd also use 201 (created) instead of no content in this context.
It looks like you are trying to write an integration test that verifies that your software wrote a record to the database with the appropriate values for percent_progress and started_at.
You are correct to be concerned about using .last in your tests like you have. If you were to run your tests in parallel on a build server, there's a good chance that two different tests would both be adding records to the database at the same time (or in an indeterminate order), causing the test to be flaky. You could resolve this potential flakiness by returning the id of the newly created record and then looking up that record in the test after the call event. But, there's a better solution...
If you were to modify your migration for the JourneyProgress model to look like this:
create_table :journey_progress do |t|
t.user_id :integer
t.journey_id :integer
t.percent_progress :float, default: 0.0
t.timestamps
end
Then, you would be guaranteed that the percent_progress field would always default to 0.0. And, you could use the ActiveRecord managed created_at timestamp in lieu of the custom started_at timestamp that you would have to manage.
So, you won't have to test that either thing got set correctly, because you can trust ActiveRecord and your database to do the right thing because they've already been thoroughly tested by their authors.
Now, your code would look more like this:
post :enroll do
journey_progress = JourneyProgress.create!(
user: current_user,
journey: journey
)
status :created
end
And, your tests would look more like what Sebastian Delgado mentioned:
it 'persists the record in database' do
expect { call }.to change { JourneyProgress.count }.by(1)
end
Related
I have a model with the following callback:
class CheckIn < ActiveRecord::Base
after_create :send_delighted_survey
def send_delighted_survey
position = client&.check_ins&.reverse&.index(self)
if position.present? && type_of_weighin.present?
survey = SurveyRequirement.find_by(position: [position, "*"], type_of_weighin: [type_of_weighin, "*"])
if survey.present?
survey.delighted_survey.sendSurvey(client: self.client, additional_properties: {delay: 3600})
end
end
end
end
I am attempting to test the line: survey.delighted_survey.sendSurvey(client: self.client, additional_properties: {delay: 3600}) to ensure that the correct delighted_survey is receiving sendSurvey.
This test passes:
let!(:week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
expect_any_instance_of(DelightedSurvey).to receive(:sendSurvey).once
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
end
However this test fails and I don't understand why
let!(:week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
expect(week_1_sr.delighted_survey).to receive(:sendSurvey).once
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
end
When I add print statements, it's definitely calling sendsurvey on week_1_sr.delighted_survey so I don't understand why the test fails.
How should I rearrange this test?
In my experience this is a common misunderstanding with expect(..).to receive when dealing with ActiveRecord.
We have to remember that the ActiveRecord object we create in our test stores a row in the database, the code in your model loads the row from the database and populates an entirely different activerecord object that is completely unconnected from the one in your test, except that they both refer to the same underlying database row.
Rspec is not "smart" about activerecord and the method you're stubbing/expecting is only on the instance of object in your test.
So how to fix this. The most direct option is to stub out the object that your code actually uses. This isn't easy, however, since it's an object returned from a method on another object returned by SurveyRequirement.find_by(...). You can do it with something like:
Option 1 - Stub everything
survey_requirement_stub = double(SurveyRequirement)
survey_stub = double(Survey)
allow(SurveyRequirement).to receive(:find_by).and_return(survey_requirement_stub)
allow(survey_requirement_stub).to receive(:delighted_survey).and_return(survey_stub)
expect(survey_stub).to receive(:sendSurvey).once
However I wouldn't recommend this. It closely ties your test to the internal implementation of your method. e.g. Adding a scope (scoped.find_by instead of find_by) breaks the test in a way that's not meaningful.
Option 2 - Test the results, not the implementation
If the point of sendSurvey is enqueuing a background job or sending an email, that may be be a better place to test that it's doing what's expected, for example:
expect { create_checkin }.to have_enqueued_job(MyEmailJob)
# or, if it sends right away
expect { create_checkin }.to change { ActionMailer::Base.deliveries.count }.by(1)
I think this approach is OK, but the implementation means that your code will be enqueuing jobs and firing emails throughout your test base. It will not be possible to create a check-in and not fire these.
This is why I strongly advise our engineers to NEVER use activerecord callbacks for business logic like this.
Instead...
Option 3 - Refactor to use service objects / interactors instead
As your application grows, using activerecord callbacks to create other records, update records, or trigger side-effects (like emails) becomes a significant anti-pattern. I'd take this as an opportunity to restructure the code to be easier to test and remove business logic from your ActiveRecord objects.
This should make each part of this easier to test (e.g. is the survey-requirement looked up correctly? Does it send?). I've run out of time but here's the general idea:
class CheckIn
def get_survey_requirement
position = client&.check_ins&.reverse&.index(self)
return unless position.present? && type_of_weighin.present?
SurveyRequirement.find_by(position: [position, "*"], type_of_weighin: [type_of_weighin, "*"])
end
end
class CheckInCreater
def self.call(params)
check_in = CheckIn.build(params)
check_in.save!
DelightedSurveySender.call(check_in)
end
end
class DelightedSurveySender
def self.call(check_in)
survey = check_in.survey_requirement&.delighted_survey
return unless survey
survey.send_survey(client: check_in.client, additional_properties: {delay: 3600})
end
end
This is happening because week_1_sr.delighted_survey in spec and survey.delighted_survey aren't the same instance. Yes, both are instances of the same class and they both represent the same record in the database and same model behavior, but they do not have the same object_id.
In your first test, you are expecting that any instance of DelightedSurvey receives the method and that's, indeed, true. But in your second spec, you expect that that exact instance receives sendSurvey.
There are many ways to rearrange your test. In fact, if you ask 100 developers how to test something, you will get 100 different answers.
There is this approach:
let(:create_week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
week_1_sr = create_week_1_sr # I don't think DRY is the better approach for testing, but it's just my opinion
allow(SurveyRequirement).to(receive(:find_by).and_return(week_1_sr))
delighted_survey_spy = instance_spy(DelightedSurvey)
allow(week_1_sr).to(receive(:delighted_survey).and_return(delighted_survey_spy))
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
expect(delighted_survey_spy).to(have_received(:sendSurvey))
end
First thing about the test I wrote: Arrange, Act and Assert. It's clear to me where I am arranging my test, where I am acting and where I am asserting.
But you can realize that this test is polluted and has some prejudicial mocks. Like:
allow(SurveyRequirement).to(receive(:find_by).and_return(week_1_sr))
It will return week_1_sr even if you pass a wrong parameter to find_by (you can workaround it using with, but it will add logic to your tests).
You can see that it's pretty hard to test and I would agree. So would you consider removing this logic to a service class or whatever?
Oh, and just a heads up: after_create will be triggered even if the record is not commited for whatever reason. So you might consider using after_create_commit
(just finished and got the notice of melcher's answer. his is better)
In my model definition, I have
# models/my_model.rb
# == Schema Information
#
# Table name: my_models
#
# id :bigint not null, primary key
# another_model_id :bigint
# field_1 :string
# field_2 :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_my_models_on_another_model_id (another_model_id) UNIQUE
class MyModel < ApplicationRecord
belongs_to :another_model
def update_from_api_response(api_response)
$stderr.puts("UPDATE")
self.field_1 = api_response[:field_1]
self.field_2 = api_response[:field_2]
end
def update_my_model!(api_response)
ApplicationRecord.transaction do
$stderr.puts("HELLO")
update_from_api_response(api_response)
$stderr.puts("WORLD")
self.save!
end
end
end
I put in some puts statements to check whether my code entered the function. If everything works alright, the program should log "HELLO", "UPDATE", then "WORLD".
In my model spec I have
# spec/models/my_model_spec.rb
RSpec.describe MyModel, type: :model do
let(:my_model) { create(:my_model) }
let(:api_response) {
{
:field_1 => "field_1",
:field_2 => "field_2",
}
}
describe("update_my_model") do
it "should update db record" do
expect(my_model).to receive(:update_from_api_response)
.with(api_response)
expect(my_model).to receive(:save!)
expect{ my_model.update_my_model!(api_response) }
.to change{ my_model.field_1 }
end
end
end
The factory object for MyModel is defined like this (it literally does not do anything)
# spec/factories/my_models.rb
FactoryBot.define do
factory :my_model do
end
end
The output from the puts (this appears before the error message)
HELLO
WORLD
Interestingly, "UPDATE" is not printed, but it passes the receive test.
The change match test fails, and the output from the console is as follows
1) MyModel update_my_model should update db record
Failure/Error:
expect{ my_model.update_my_model(api_response) }
.to change{ my_model.field_1 }
expected `my_model.field_1` to have changed, but is still nil
# ./spec/models/my_model_spec.rb
# ./spec/rails_helper.rb
I suspected that it might have something to do with me wrapping the update within ApplicationRecord.transaction do but removing that does nothing as well. "UPDATE" is not printed in both cases.
I've also changed the .to receive(:update_from_api_response) to .to_not receive(:updated_from_api_response) but it throws an error saying that the function was called (but why is "UPDATE" not printed then?). Is there something wrong with the way I'm updating my functions? I'm new to Ruby so this whole self syntax and whatnot is unfamiliar and counter-intuitive. I'm not sure if I "updated" my model field correctly.
Thanks!
Link to Git repo: https://github.com/jzheng13/rails-tutorial.git
When you call expect(my_model).to receive(:update_from_api_response).with(api_response) it actually overrides the original method and does not call it.
You can call expect(my_model).to receive(:update_from_api_response).with(api_response).and_call_original if you want your original method to be called too.
Anyway, using "expect to_receive" and "and_call_original" rings some bells for me, it means you are testing two different methods in one test and the tests actually depends on implementation details instead of an input and an output. I would run two different tests: test that "update_from_api_response" changes the fields you want, and maybe test that "update_my_model!" calls "update_from_api_response" and "save!" (no need to test the field change, since that would be covered on the "update_from_api_response" test).
Thank you, the separate Github file works wonders.
This part works fine:
Put it in a separate expectation and it works fine:
describe("update_my_model") do
it "should update db record" do
# This works
expect{ my_model.update_my_model!(api_response) }.to change{ my_model.field_one }
end
end
How is it triggered?
But here is your problem:
expect(my_model).to receive(:update_from_api_response).with(api_response)
expect(my_model).to receive(:save!)
This means that you are expecting my model to have update_from_api_response to be called with the api_response parameter passed in. But what is triggering that? Of course it will fail. I am expecting my engine to start. But unless i take out my car keys, and turn on the ignition, it won't start. But if you are expecting the car engine to start without doing anything at all - then of course it will fail! Please refer to what #arieljuod has mentioned above.
Also why do you have two methods: update_from_api_response and update_my_model! which both do the same thing - you only need one?
How could I write a test to find the last created record?
This is the code I want to test:
Post.order(created_at: :desc).first
I'm also using factorybot
If you've called your method 'last_post':
def self.last_post
Post.order(created_at: :desc).first
end
Then in your test:
it 'should return the last post' do
expect(Post.last_post).to eq(Post.last)
end
On another note, the easiest way to write your code is simply
Post.last
And you shouldn't really be testing the outcome of ruby methods (you should be making sure the correct ruby methods are called), so if you did:
def self.last_post
Post.last
end
Then your test might be:
it 'should send the last method to the post class' do
expect(Post).to receive(:last)
Post.last_post
end
You're not testing the outcome of the 'last' method call - just that it gets called.
The accepted answer is incorrect. Simply doing Post.last will order the posts by the ID, not by when they were created.
https://apidock.com/rails/ActiveRecord/FinderMethods/last
If you're using sequential IDs (and ideally you shouldn't be) then obviously this will work, but if not then you'll need to specify the column to sort by. So either:
def self.last_post
order(created_at: :desc).first
end
or:
def self.last_post
order(:created_at).last
end
Personally I'd look to do this as a scope rather than a dedicated method.
scope :last_created -> { order(:created_at).last }
This allows you to create some nice chains with other scopes, such as if you had one to find all posts by a particular user/account, you could then chain this pretty cleanly:
Post.for_user(user).last_created
Sure you can chain methods as well, but if you're dealing with Query interface methods I feel scopes just make more sense, and tend to be cleaner.
If you wanted to test that it returns the correct record, in your test you could do something like:
let!(:last_created_post) { factory_to_create_post }
. . .
it "returns the correct post"
expect(Post.last_post).to eq(last_created_post)
end
If you wanted to have an even better test, you could create a couple records before the last record to verify the method under test is pulling the correct result and not just a result from a singular record.
I am successfully testing whether certain properties of an ActiveRecord model are updated. I also want to test that ONLY those properties have changed. I was hoping I could hook into the model's .changes or .previous_changes methods to verify the properties I expect to change are the only ones being changed.
UPDATE
Looking for something equivalent to the following (which doesn't work):
it "only changes specific properties" do
model.do_change
expect(model.changed - ["name", "age", "address"]).to eq([])
end
Try something like this
expect { model.method_that_changes_attributes }
.to change(model, :attribute_one).from(nil).to(1)
.and change(model, :attribute_two)
If the changes are not attributes, but relations you might need to reload the model:
# Assuming that model has_one :foo
expect { model.method_that_changes_relation }
.to change { model.reload.foo.id }.from(1).to(5)
EDIT:
After some clarification from the OP comment:
You can do this then
# Assuming, that :foo and :bar can be changed, and rest can not
(described_class.attribute_names - %w[foo bar]).each |attribute|
specify "does not change #{attribute}" do
expect { model.method_that_changes_attributes }
.not_to change(model, attribute.to_sym)
end
end
end
This is essentially what you need.
This solution has one issue though: it will call method_that_changes_attributes for each attribute, and this can be inefficient. If that's the case - you may want to make your own matcher that accepts an array of methods. Start here
Maybe this can help:
model.do_change
expect(model.saved_changes.keys).to contain_exactly 'name', 'age', 'address'
this is supposed to work with .previous_changes too.
if the changes are not saved then .changed should work.
At the end of the day this is really dependent on how things happen on do_change
I have a class which performs several database operations, and I want to write a unit test which verifies that these operations are all performed within a transaction. What's a nice clean way to do that?
Here's some sample code illustrating the class I'm testing:
class StructureUpdater
def initialize(structure)
#structure = structure
end
def update_structure
SeAccount.transaction do
delete_existing_statistics
delete_existing_structure
add_campaigns
# ... etc
end
end
private
def delete_existing_statistics
# ...
end
def delete_existing_structure
# ...
end
def add_campaigns
# ...
end
end
Rspec lets you assert that data has changed in the scope of a particular block.
it "should delete existing statistics" do
lambda do
#structure_updater.update_structure
end.should change(SeAccount, :count).by(3)
end
...or some such depending on what your schema looks like, etc. Not sure what exactly is going on in delete_existing_statistics so modify the change clause accordingly.
EDIT: Didn't understand the question at first, my apologies. You could try asserting the following to make sure these calls occur in a given order (again, using RSpec):
EDIT: You can't assert an expectation against a transaction in a test that has expectations for calls within that transaction. The closest I could come up with off the cuff was:
describe StructureUpdater do
before(:each) do
#structure_updater = StructureUpdater.new(Structure.new)
end
it "should update the model within a Transaction" do
SeAccount.should_receive(:transaction)
#structure_updater.update_structure
end
it "should do these other things" do
#structure_updater.should_receive(:delete_existing_statistics).ordered
#structure_updater.should_receive(:delete_existing_structure).ordered
#structure_updater.should_receive(:add_campaigns).ordered
#structure_updater.update_structure
end
end
ONE MORE TRY: Another minor hack would be to force one of the later method calls in the transaction block to raise, and assert that nothing has changed in the DB. For instance, assuming Statistic is a model, and delete_existing_statistics would change the count of Statistic in the DB, you could know that call occurred in a transaction if an exception thrown later in the transaction rolled back that change. Something like:
it "should happen in a transaction" do
#structure_updater.stub!(:add_campaigns).and_raise
lambda {#structure_updater.update_structure}.should_not change(Statistic, :count)
end