I'm new to Rails testing, I have this question:
Let's suppose this simple scenario where I have in my controller:
class UsersController < ApplicationController
def create
#user = User.create(params[:user])
if params[:user][:birth_date]
Birthday.create(:title => "#{user.first_name} #{user.last_name}'s Birthday!", :date => params[:user][:birth_date].to_date, :user_id => #user.id)
end
end
def update
#user = User.find(params[:id])
#user.update_attributes(params[:user])
if params[:user][:birth_date]
#birthday = Birthday.find_by_user_id(#user.id)
#birthday.update_attributes(:date => params[:user][:birth_date].to_date) if #birthday
end
end
end
I want to test that every time a user is created the birthday event is created and that it's attributes are properly set. In my particular (real) case I have that a new object is created (or updated) when another object is created (or updated), and a lot of attributes are calculated and automatically set. I need to test that they are set correctly. How can I test it?
Test the Correct Objects
You want to test the object that is populating your User data, or possibly the callbacks in your User model. This is not generally a controller concern, especially if you follow the "fat model, skinny controller" paradigm.
In your specific case, your controller is calling Birthday#update_attributes, but the real changes are happening elsewhere, so that's where I'd test them. The only really useful tests for this particular controller would be ensuring that nothing is raised when you create or update your model data, but that's more of an integration test than a unit test.
If you are using Test::Unit or RSpec you can access the global variables through the "assigns" method. As an example, you could use something like this:
post :update, :id => 1, :user => {:birthdate => '1/1/2000'}
assert_equal Date.new(2000, 1, 1), assigns(:user).birthday.date
You can also test that the database was updated correctly:
user = User.find(1)
assert_equal Date.new(2000, 1, 1), user.birthday.date
Related
I have a n00b question. I'm using Rails 5, and would like to have example data in the application. When a user creates a new project, the project should already contain sample "tasks" that the user can delete or edit.
I know I can use seeds.rb to create sample data in my development environment. What is the best way to do it in a production environment for new users, and how? Should I use seeds.rb, a migration, or a rake task?
Example controller:
def create
#project = Project.new(project_params)
#project.user = current_user
if #project.save
// add sample content
redirect_to #project
else
render :new
end
end
In the Project model:
belongs_to :user
has_many :tasks, dependent: :destroy
When a new user joins and creates a new project, how do I add sample "tasks" automatically on the new project that the user creates?
UPDATE:
To create a task, I need a description and the current user's id (I'm using Devise, so I can use the current_user helper), for example:
#project.tasks.create!(description: "hello", user_id: current_user.id)
You could build a simple ServiceObject that does the job. It allows you to keep your controller skinny and you can user your current_user Devise helper to keep track of which user is creating the project
if #project.save
SetupBaseProject.new(project).call
redirect_to #project
else
# handle errors
end
In app/services/setup_base_project.rb
class SetupBaseProject
def initialize(project, user)
#project = project
end
def call
# Create example tasks and any additional setup you want to add
#project.tasks.create(description: 'Hello World', user: #project.user)
end
end
There are two possible scenarios considering your question.
The first project created by a user needs to have sample tasks included by default
Whenever a new project is created, sample tasks are created by default. Irrespective of the user is new user/existing user.
For first scenario,
We need to track whether project is created by new user by adding a boolean field to user, for example: new_user which defaults true.
We can use active record callbacks for generating sample tasks after project is created.
For Example,
Project Model :
belongs_to :user
has_many :tasks, dependent: destroy
after_create :generate_tasks
def generate_tasks
if self.user.new_user #This conditional block can be modified as needed
(1..3).each do |n|
self.tasks.create!(description: "Sample task #{n}", user_id: self.user.id)
end
end
end
For the second scenario,
We can use the same projects model file and just remove the conditional statement which will help create sample tasks by after project is created.
If you need any clarification, please comment out.
I've done this quite a few times in the past.
From my experience, you eventually have to give other people the ability to manage those defaults (product owners, marketing, etc)
What I've done in the past is to have a test user with a project that acts as 'the default' project.
Whenever anyone wants to create a new project, you clone it.
I used https://github.com/amoeba-rb/amoeba for that. It offers out of the bow way to override attributes that I'd want to change and can cascade the cloning to any associations you'd want to clone.
Say sample data is on model Detail which was populated with seeds.rb and belongs to 'Project'. You can dup that record and asign it to the new project (not tested):
def create
#project = Project.new(project_params)
#project.user = current_user
#project.details << Detail.find_by_name('sample').dup
if #project.save
redirect_to #company
else
render :new
end
end
Also, consider use a transaction when saving data on more than one model.
Full disclosure, I work in Rails 4...
If it were me, I would use FactoryBot to get the dummy data you want. Factories are great for testing so if you use them for testing, why not borrow them for this? This post shows an example where someone wanted to mock dummy data in console, same ideas could apply for you here.
Once you've got your factories mocked up... maybe for tasks something like:
require 'faker'
FactoryBot.define do
factory :task do
transient do
parent_project { nil }
end
description { Faker::Hacker.say_something_smart }
project_id { parent_project.id }
end
end
Maybe create a method in the project model like:
def create_dummy_data
require 'factory_bot'
require 'faker'
include FactoryBot::Syntax::Methods
# create_list will spit out 3 tasks associated with your project
create_list(:task, 3, parent_project: self)
end
Then in your example: after calling save...
if #project.save
#project.create_dummy_data
redirect_to #company
else
I can't think of a reason you couldn't go this route... noodling around in console I didn't have any problems, but I'd look at this answer as a starting point and not a final solution =P
I have two models, User and PushupReminder, and a method create_a_reminder in my PushupReminder controller (is that the best place to put it?) that I want to have create a new instance of a PushupReminder for a given user when I pass it a user ID. I have the association via the user_id column working correctly in my PushupReminder table and I've tested that I can both create reminders & send the reminder email correctly via the Rails console.
Here is a snippet of the model code:
class User < ActiveRecord::Base
has_many :pushup_reminders
end
class PushupReminder < ActiveRecord::Base
belongs_to :user
end
And the create_a_reminder method:
def create_a_reminder(user)
#user = User.find(user)
#reminder = PushupReminder.create(:user_id => #user.id, :completed => false, :num_pushups => #user.pushups_per_reminder, :when_sent => Time.now)
PushupReminderMailer.reminder_email(#user).deliver
end
I'm at a loss for how to run that create_a_reminder method in my code for a given user (eventually will be in a cron job for all my users). If someone could help me get my thinking on the right track, I'd really appreciate it.
Thanks!
Edit: I've posted a sample Rails app here demonstrating the stuff I'm talking about in my answer. I've also posted a new commit, complete with comments that demonstrates how to handle pushup reminders when they're also available in a non-nested fashion.
Paul's on the right track, for sure. You'll want this create functionality in two places, the second being important if you want to run this as a cron job.
In your PushupRemindersController, as a nested resource for a User; for the sake of creating pushup reminders via the web.
In a rake task, which will be run as a cron job.
Most of the code you need is already provided for you by Rails, and most of it you've already got set in your ActiveRecord associations. For #1, in routes.rb, setup nested routes...
# Creates routes like...
# /users/<user_id>/pushup_reminders
# /users/<user_id>/pushup_reminders/new
# /users/<user_id>/pushup_reminders/<id>
resources :users do
resources :pushup_reminders
end
And your PushupRemindersController should look something like...
class PushupRemindersController < ApplicationController
before_filter :get_user
# Most of this you'll already have.
def index
#pushup_reminders = #user.pushup_reminders
respond_with #pushup_reminders
end
# This is the important one.
def create
attrs = {
:completed => false,
:num_pushups => #user.pushups_per_reminder,
:when_sent => Time.now
}
#pushup_reminder = #user.pushup_reminders.create(attrs)
respond_with #pushup_reminder
end
# This will handle getting the user from the params, thanks to the `before_filter`.
def get_user
#user = User.find(params[:user_id])
end
end
Of course, you'll have a new action that will present a web form to a user, etc. etc.
For the second use case, the cron task, set it up as a Rake task in your lib/tasks directory of your project. This gives you free reign to setup an action that gets hit whenever you need, via a cron task. You'll have full access to all your Rails models and so forth, just like a controller action. The real trick is this: if you've got crazy custom logic for setting up reminders, move it to an action in the PushupReminder model. That way you can fire off a creation method from a rake task, and one from the controller, and you don't have to repeat writing any of your creation logic. Remember, don't repeat yourself (DRY)!
One gem I've found quite useful in setting up cron tasks is the whenever gem. Write your site-specific cron jobs in Ruby, and get the exact output of what you'd need to paste into a cron tab (and if you're deploying via Capistrano, total hands-off management of cron jobs)!
Try setting your attr_accessible to :user instead of :user_id.
attr_accessible :user
An even better way to do this however would be to do
#user.pushup_reminders.create
That way the user_id is automatically assigned.
Use nested routes like this:
:resources :users do
:resources :pushup_reminders
end
This will give you params[:user_id] & params[:id] so you can find your objects in the db.
If you know your user via sessions, you won't need to nest your routes and can use that to save things instead.
Using restful routes, I would recommend using the create action in the pushup_reminders controller. This would be the most conventional and Restful way to do this kind of object creation.
def create
#user = User.find(params[:user_id]
#reminder = #user.pushup_reminders.create()
end
If you need to check whether object creation was successful, try using .new and .save
I am building a Ruby on Rails app with the usual assortment of models, views and controllers.
The 'create' action in one of my controllers is supposed to create an instance of two different models. Here's my code:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
... .save conditions appended ...
end
So, when you call 'create' through the LeaguesController via a POST request to /leagues, you get a new instance of League and a new instance of User. I also want the new User instance to inherit the ID of the new League instance, so it can be used as the foreign key to link the instances together. This is accomplished with:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
The User model already belongs_to the League model, which has_many users.
The above code works just fine and dandy, verified via manual testing. However, I can't for the life of me figure out how to automate these tests with Rspec. I'm trying to be a good boy and use test-driven design, but this has me stumped.
The issue is that I can't figure out how to access the attributes of the newly created instances of League and User in my tests. I am attempting to do so using the following code:
describe LeaguesController do
describe 'new league and user' do
it 'should create a new user with a league_id equal to the new leagues id'
#league_attr = { :name => "myleague", :confirmation_code => "mycode", :id => 5}
#user_attr = { :username => "myname", :password => "mypass"}
post :create, :user => #user_attr, :league => #league_attr
assigns(:league_id).should eql(5)
end
end
end
But the test returns nil for the value of :league_id
I'm new to both programming in general and Rspec in particular, so I really appreciate any help someone might offer!
You cannot assign :id with new. Try this:
def create
#league = League.new(params[:league])
#league.id = params[:league][:id] if params[:league][:id]
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
That said, I wonder how come it works in the browser.
Also, you better off using FactoryGirl or Fixtures to assign data to models when testing.
Have you tried pry?
Whenever I discover something like this I find it very handy to be able to insert a brakepoint via Pry (or Ruby-Debug) so I can inspect the variables and their behavior.
I suspect putting in a binding.pry between #league_id = #league.id and #user.update_attribute('league_id', #league_id) may very well shed some light on the issue.
Also note that user will automatically inherit the #league_id when you persist it via the #league.save call. (that's the idea behind #league.users.build(..) - it will set the required relationships correctly upon persistance.
Here's more of an academic question for you guys. Say I want to create a model in a ruby on rails app to track simple views information. I would like to record the user_id, the URI for the page viewed, and keep track of the number of times the user has visited a page.
Model A: One way to do this would be to create a model View with attributes user_id and page (records the uri), and then create a new entry every time a user opens a page.
Model B: A second way to do this would be to add an attribute "page_views" to the model, to track the number of times the user has accessed that page.
Pros and Cons: Model A would have more information recorded and lead to a larger db than Model B. However, Model B would require that a controller search for an existing user-page combination, and either add views to that entry, or create a new one. This leads to a smaller database, but may be worse in scale due to the need to search for existing entries.
So my question to you guys is: which is more important? Are my thoughts wrong? Am I missing something here (other performance considerations overlooked?)
NoSQL approach to tracking user activity:
model app/models/user.rb
class User < ActiveRecord::Base
include UserModules::Tracker
...
end
mixin app/models/user_modules/tracker.rb
module UserModules
module Tracker
def get
key = "user_" + self.id.to_s
arr = Resque.redis.lrange(key, 0, -1)
arr.map{|i| JSON.parse(i)}
end
def put(controller_name, action_name, details="")
key = "user_" + self.id.to_s
created = Time.now.to_formatted_s(:db)}.to_json
# silent exception handle, so you can do not run Redis localy
begin
Resque.redis.rpush key, {
:controller_name => controller_name,
:action_name => action_name,
:details => details,
:created_at => created
rescue
nil
end
end
end
end
controller app/controller/dashboard.rb
class Dashboard < ApplicationController
after_filter :track, :only => :show
# this action will be tracked
def show
end
# this action will show tracking
def logs_show
render :json => current_user.get
end
...
private
def track
details = "any details...."
current_user.put(controller_name, action_name, details)
end
end
You need to have Redis installed, I prefer to use Resque as common way to setup and initialize Redis via Resque.redis, because it will help you to browse your tracking with resque-web
On of the way to setup Redis is in my gist
The idea is as follows: when visiting a purchase page, the pre-initialized (using before_filter) #purchase variable receives save if the item in question is not free.
I make two gets, one for a paid item and one for a free item. purchase.expects(:save).returns(true) expects :save to be called only once, so the below test works.
But this is really really ugly. The test it incredibly lengthy. What would be a better way to do this? Should I mock the find_or_initialize method? If so, how would I set the #purchase instance variable?
Sorry for the ugly code below...
def test_new_should_save_purchase_if_not_free
user = users(:some)
purchase = user.purchases.build
#controller.stubs(:current_user).returns(user)
purchases_mock = mock
user.stubs(:purchases).returns(purchases_mock)
purchases_mock.stubs(:build).returns(purchase)
purchase.expects(:save).returns(true)
get :new, :item_id => items(:not_free).id, :quantity => 10
get :new, :item_id => items(:free).id, :quantity => 400
end
def new
#purchase.quantity = params[:quantity]
#purchase.item = Item.find(params[:item_id])
unless #purchase.item.free?
#purchase.save
end
end
def find_or_initialize
#purchase = params[:id] ? current_user.purchases.find(params[:id]) : current_user.purchases.build
end
It looks like you're already using fixtures for your items, why not just use a fixture for the Purchase as well? There is no need to go through all that effort to stub the user.