test "TITLE" do
products = Product.all
total_price = products.to_a.sum(&:price)
expected_total = total_price * 100
post product_path, price: 500, product: "random"
assert_template :index
assert_equal expected_total, products.to_a.map(&:reload).sum(&:price)
end
I'm submitting my product randomly and it's working fine. but the price is not display after reload the products. It's display as 0. When I print the value in the form then it's display value in the price field.
On the first line, you don't have any products yet, so Product.all will return an empty collection. Therefore expected_total will be zero.
If you move this this setup code to after the post call then it should pass.
(However, your test is not actually verifying the implementation of the calculation).
Related
Let me present my problem into the simplest example I can come up with that is somewhat similar.
I have a database table "Article" with an integer attribute named "weight" and a string attribute named "name"
I have 10 Articles in the database with varying weights and names
I get a GET request that includes "name" as the parameter
Among the 10 Articles, I find the Articles with the same name. I found 4 Articles.
Among the 4 Articles, I choose one that is randomly chosen, but the randomness is weighted where a higher weight gives a higher chance of being chosen. So if the four weights are 1,3,5,7. I generate a random number between 0 and the sum of the weights (which is 1+3+5+7=16), and use that number to decide which Article is selected. So if the random number I got was 0, then I select the first Article. If the random number I got was 1~3, I select the second Article. If the random number I got was 4~8, I select the third Article. If the random number I got was 9~15, I select the fourth Article.
I send that Article as a response for the GET request.
As you can see, the response to a GET request with a specific "name" as parameter would not always be the same. However, all examples I find about testing Controllers rely on the response of the request being deterministic.
So I'd like to get some help on how I would go about designing my test code for this.
My current thoughts for tests are as following:
In terms of the response for the GET request, if the GET query does not include the name parameter, then I expect a HTTP Status code of 400 with a message "Bad parameters"
If the GET includes the name parameter, then I expect a HTTP Status code of 200 (but don't do any additional checks on the content)
If the GET includes a name that does not match with any of the Articles, then I expect a HTTP Status code of 200 and "null" as the response message.
I do a GET request with a name, I get the correct list of Articles that matches the "name" before I do the random selection.
I create 4 Article objects with the same "name" and each with weight 1,2,3,4 (sum of 10) respectively. I do 10,000 runs on the logic that does the weighted random selection. I check if the number of times I get the first Article is between 700 to 1300 (about 1/10 of 10,000), if the number of times I get the second Article is between 1700 to 2300 (about 2/10 of 10,000), if the number of times I get the third Article is between 2700 to 3300 (about 3/10 of 10,000), if the number of times I get the fourth Article is between 3700 to 4300 (about 4/10 of 10,000).
One problem is that despite trying to read on how to write tests on Ruby on Rails, I'm still not exactly sure how to do 4 and 5. 4 and 5 requires that I test things "within" the logic (instead of using the result of executing the entire logic). It seems like I need to break down the logic in smaller functions that can be called for testing 4 and 5. Is that correct? What else are things I should be testing?
Here is a simple code I created so that we can follow along for this example. The full code is here: https://github.com/maruhanpark/example
You can run it with docker compose.
The tests I explained above looks like the following in code:
require 'test_helper'
class ArticleControllerTest < ActionDispatch::IntegrationTest
test "having no name parameter returns Bad Parameters" do
get "/article"
assert_response(400)
assert_equal "{\"message\":\"Bad parameters\"}", #response.body
end
test "having name parameter returns 200" do
get "/article?name=Bob"
assert_response(200)
end
test "having empty name parameter returns 200 and null" do
get "/article?name="
assert_response(200)
assert_equal "null", #response.body
end
test "getSelectedArticles returns the correct list of articles" do
end
test "randomWeight selects properly based on weight" do
end
end
As you can see, I wrote the cases for the first three I explained, but left out cases 4 and 5.
For the Controller, I split off the logic that I want to test as separate methods. The two methods I created are each for the test cases 4 and 5. I'm not sure if it's necessary for me to create separate methods to test that logic.
class ArticleController < ApplicationController
def getSelectedArticles(name:)
#logger.debug "===== #{Article.where(name: name)}"
return Article.where(name: name)
end
#Returns the index of the finally selected article
def randomWeight(articles:)
sum = 0
articles.each { |a| sum += a.weight }
randNum = rand(sum)
returnIndex = 0
for i in 0 ... articles.size
randNum -= articles[i].weight
if (randNum < 0)
returnIndex = i
break
end
end
return returnIndex
end
def article
if params[:name]
selectedArticles = getSelectedArticles(name: params[:name])
finalArticle = selectedArticles[randomWeight(articles: selectedArticles)]
render json: finalArticle, status: :ok
else
render json: {message: "Bad parameters"}, status: :bad_request
end
end
end
One problem I'm facing for trying to implement test 4 is that I need to somehow get the code to have a breakpoint on the getSelectedArticles method after calling GET. I'm not sure if that's possible, or if I have to test it only as sort of a unit-test where I'm specifically calling the method and seeing that the method returns the expected thing.
I read your quest and noted the following things we could be improving on your code:
Please use RESTful routes, ie in your routes.rbĀ please write resources :articles, only: [:show], have your controller called ArticlesController (note the plural) and your route will be /articles/:id
Please use two space indentation and snake_case for method and variable names
Use of privateĀ is encouraged to separate methods
No need for "return" on method's last statements
Selecting the random article in ruby might not be really performant if you have a lot of articles, I'd recommend doing it on the database: it'd be a bit more work on SQL skills but but it would be faster and scale better
I would encourage you to separate the controller stuff from your service so it's easier to test and develop
class SearchArticles
def call(params)
# All your logic here
end
end
class ArticlesController < ApplicationController
def article
render json: { message: 'Bad parameters' }, status: :bad_request unless params[:name].present?
final_article = SearchArticles.new.call(params)
render json: final_article, status: :ok
end
Anyway here's my first pass of improvements:
class ArticlesController < ApplicationController
def article
render json: { message: 'Bad parameters' }, status: :bad_request unless params[:name].present?
selected_articles = search_articles(name: params[:name])
final_article = selected_articles[random_weight(articles: selected_articles)]
render json: final_article, status: :ok
end
private
def search_articles(name:)
Article.where('name LIKE ?', name)
end
def random_weight(articles:)
sum = articles.pluck(:weight).sum
rand_num = rand(sum)
return_index = 0
# This could be improved!
for i in 0...articles.size
rand_num -= articles[i].weight
if (rand_num < 0)
return_index = i
break
end
end
return_index
end
end
The test
As I don't know much about minitest, I'll write my answer for RSpec in hope you'll be able to convert it on your own (warning, untested!):
describe ArticlesController do
describe '#article' do
before do
create(:article, id: 11, name: 'Bob 11', weight: 1)
create(:article, id: 22, name: 'Bob 22', weight: 3)
create(:article, id: 33, name: 'Bob 33', weight: 5)
create(:article, id: 44, name: 'Bob 44', weight: 7)
end
it 'returns the correct article' do
{
0 => 11,
1 => 22,
3 => 22,
4 => 33,
8 => 33,
9 => 44,
15 => 44,
}.each do |random_number, expected_article_id|
allow_any_instance_of(Object)
.to receive(:rand).and_return(random_number)
get "/article?name=Bob"
result = JSON.parse(response.body).id # please continue from here
expect(result).to eq(expected_article_id)
end
end
end
end
Please note none of above is in great shape, it should be taken just as a good starting point.
Best of luck mate!
My app for teachers includes a method to delete a student from a seminar (a class but I didn't want to use the word "class") by deleting that student's record from the join table. I don't delete the student directly, but I delete an "Aula," which is a record in the student/seminar join table. I'm trying to write a test to ensure that when a student is deleted, that student's id is also deleted from the seating chart array. This function appears to be working correctly in development, but not in testing.
The Test
test "Remove student from class period and seating chart" do
log_in_as(#teacher_user)
get seminar_path(#seminar)
seating = [#student.id, #other_student.id]
#seminar.update(:seating => seating)
assert_difference ['Aula.count','#student.aulas.count', '#seminar.students.count'], -1 do
delete aula_path(#aula)
end
puts "In test"
puts #seminar.seating
assert_redirected_to scoresheet_url(#seminar)
end
The Destroy Method in the Aula Controller
def destroy
thisAula = Aula.find(params[:id])
#seminar = Seminar.find(thisAula.seminar_id)
#Remove student from seating chart
seating = #seminar.seating
seating.delete(thisAula.student_id)
#seminar.update(:seating => seating)
puts "In Controller"
puts #seminar.seating
thisAula.destroy
#Redirect
flash[:success] = "Student removed from class period"
redirect_to scoresheet_url(#seminar)
end
Once I fix this error, I would like to include the seating chart count within the test, like so:
assert_difference ['Aula.count','#student.aulas.count', '#seminar.students.count', '#seminar.seating.count'], -1 do
But the test fails there, because the seating array is not decreasing its count.
I've tried those "puts" lines to figure out what is happening.
Thank you in advance for any insight.
-Jeff
Running the test puts:
In Controller
614857506
In Test
45061424
614857506
This shows that the student's id is deleted from the seating array like it should be. But when the program returns to the test, the seating array has reverted to its original form, which includes the id of the deleted student.
Got it. It was a case of needing to reload the model.
#seminar.reload
I'm new to Ruby on Rails and reading the book "Agile Web Development with Rails 4". As doing the "Playtime" exercises at the end of chapter 10 (Iteration E3 - Finishing the Cart), I stumbled across some problems.
One of it is in the 2nd exercise, where one should create unit tests to add unique and duplicate products to some shopping cart.
When one adds a product to that cart, it might be the first product of that kind and so the amount is one, but every additional add-operation increases the quantity. This works fine in browser-testing, but my test-cases fail.
Testcase:
test 'duplicates must not be saved as a new line item' do
# create cart and add one product
cart = new_cart_with_one_product(:ruby)
assert cart.save
assert_equal 1, cart.line_items.count
assert_equal 1, cart.line_items.find_by(
product_id: products(:ruby).id).quantity
assert_equal 49.50, cart.total_price.to_f
# ----------------------------------------------------------------
# create a second (actually the same product) and add it to cart:
item = products(:ruby)
cart.add_product(item.id, item.price)
assert cart.save
assert_equal 1, cart.line_items.count, 'duplicate saved as new line'
# test FAILS at the next two lines:
assert_equal 2, cart.line_items.find_by(product_id: item.id).quantity,
'quantity has not been increased'
assert_equal 99.00, cart.total_price.to_f, 'total price is wrong'
end
It tells me that the expected value is 2, but the actual value is 1.
So the quantity has not been increased. The total price does not change either, though both things work in the development-environment.
Here is the code of the Cart-Model:
class Cart < ActiveRecord::Base
has_many:line_items, dependent: :destroy
def add_product(product_id, product_price)
current_item = line_items.find_by(product_id: product_id)
if current_item
current_item.quantity +=1
else
# create a new line_item
current_item = line_items.build(product_id: product_id,
price: product_price)
end
current_item
end
def total_price
line_items.to_a.sum {|item| item.total_price }
end
end
I am using Rails 4.2.5 on Ruby 2.2.3.
I hope somebody can help me with that, because I do not understand why this is happening in the test-environment and using rake test only. If you need any additional code, please let me know.
I finally found my mistake:
The quantity is incremented in the model, for sure, but it isn't saved there.
It is saved in the controller, so this step is totally bypassed in my test because it just tests the model itself.
To fix the test, I changed cart.add_product(item.id, item.price)to cart.add_product(item.id, item.price).save to fix that issue.
I also reloaded the cart before testing, because otherwise it calculates the old total price (thanks to #PrakashMurthy though it should solve another issue, but at the end it helped :-) ).
The (working) Testcase looks like this, now:
test 'duplicates must not be saved as a new line item' do
# create cart and add one product
cart = new_cart_with_one_product(:ruby)
assert cart.save
assert_equal 1, cart.line_items.count
assert_equal 1, cart.line_items.find_by(
product_id: products(:ruby).id).quantity
assert_equal 49.50, cart.total_price.to_f
# ----------------------------------------------------------------
# create a second (actually the same product) and add it to cart:
item = products(:ruby)
# the following two lines do the trick:
cart.add_product(item.id, item.price).save
cart.reload
assert cart.save
assert_equal 1, cart.line_items.count, 'duplicate saved as new line'
assert_equal 2, cart.line_items.find_by(product_id: item.id).quantity,
'quantity has not been increased'
assert_equal 99.00, cart.total_price.to_f, 'total price is wrong'
end
I've been creating a cart feature in rails and I have the following models:
Cart:
class Cart < ActiveRecord::Base
has_many :items
end
Item:
class Item < ActiveRecord::Base
belongs_to :cart
belongs_to :product
end
An item also has a quantity attribute.
Now, I have an instance method on cart that given an item will either a) save the item to the database and associate it with the cart or b) if the item with the product_id already exists, simply update the quantity.
The code for this is below:
def add_item(item)
if(item.valid?)
current_item = self.items.find_by(product_id: item.product.id)
if(current_item)
current_item.update(quantity: current_item.quantity += item.quantity.to_i)
else
self.items << item
end
self.save
end
end
And this works fine.
However, I wanted to test this in the console so i opened up the console in sandbox mode and ran the following commands:
cart = Cart.new #create a cart
cart.add_item(Item.new(product: Product.find(1), quantity: 5)) #Add 5 x Product 1
cart.items #lists items, I can see 5 x Product 1 at this point.
cart.add_item(Item.new(product: Product.find(1), quantity: 3)) #Add 3 x Product 1
cart.items #lists items, still shows 5 x Product 1, not 8x
cart.items.reload #reload collectionproxy
cart.items #lists items, now with 8x Product 1
Here i create a cart, add a purchase of 5 x Product 1 and i can see this in the cart.items. If then add another purchase of 3 x Product 1, the cart.items still lists the purchase as 5 x Product 1 until i manually reload the collection proxy.
I can add more products and these will show up, it is just when updating an existing one, it does not update the collection.
I have tests around this method too which pass.
before(:each) do
#cart = create(:cart)
#product = create(:product)
#item = build(:item, product: #product, quantity: 2)
end
context "when the product already exists in the cart" do
before(:each) {#cart.add_item(#item)}
it "does not add another item to the database" do
expect{#cart.add_item(#item)}.not_to change{Item.count}
end
it "does not add another item to the cart" do
expect{#cart.add_item(#item)}.not_to change{#cart.items.count}
end
it "updates the quantity of the existing item" do
#cart.add_item(#item)
expect(#cart.items.first.quantity).to eq 4
end
end
context "when a valid item is given" do
it "adds the item to the database" do
expect{#cart.add_item(#item)}.to change{CartItem.count}.by(1)
end
it "adds the item to the cart" do
expect{#cart.add_item(#item)}.to change{#cart.items.size}.by(1)
end
end
What i want to know is, why do i have to reload the CollectionProxy when I use this method in the console?
Association caches the results of the query to achieve better performance. When you call #cart.item for the first time it will call the db to get all the items associated with given cart and it will remember its output (in internal variable called 'target'), hence every time you call it after this initial call it will give you same results without calling db at all. The only way to force it to go again to db is to clear that target variable - this can be done with reload method or passing true to association call #car.items(true).
The reason why you don't need to reload association in you rspec tests is because you are not calling items on any object twice. However if you write test like:
it 'adds an item if it is not in the cart' do
before_count = #cart.items.size # size forces the association db call
#cart.add build(:item)
after_count = #cart.items.size # items has been fetched from db, so it will return same set of results
after_count.should_not eq before_count
end
That test will fail, since you are calling items twice on the same object - and hence you will get same results. Note, that using count instead of size will make this test to pass, because count is altering the SQL query itself(which results are not being cached) while size is being delegated to the association target object.
I'm having trouble testing this code in rspec - based on the error the test gives me, I know the test is written (more or less) correctly - as the data it's expecting is correct, it's just not getting it for some reason. I should also note that the code works in the browser.
edit: apologies if this was unclear. In this controller (evaluations_controller), the user iterates through each student in a given group and evaluates their progress against a set of goals. In the new action, #student = groups.student.first - when evaluation data for that student has been saved successfully in the create action, the student_id incremented by 1, and the new student_id is passed to the new action again (so the next student can be evaluated) - this loops until there are no more students.
What I'm trying to test is that the student_id is being successfully incremented after evaluation has been saved in the create action.
Code:
def create
...
if #evaluation.save
#id = params[:student_id]
#id = #id.to_i + 1
redirect_to evaluate_path({ student_group_id: #student_group, student_id: #id})
else
...
end
end
Rspec test:
it "should load the next student" do
#set #id to current student.id +1
#id = #student.id
#id = #id.to_i + 1
#post :create
post :create, {student_group_id: #student_group, student_id: #student, evaluation: #attr}
controller.params[:student_id].should eql #id
end
Error:
Failure/Error: controller.params[:student_id].should eql #id expected: 2 got: "1"
Your code appears to be flawed, and consequently your test is not clear.
From gleaning over the code, I understand you want to use some type of next/previous student functionality. It appears you are hacking around your controller test to achieve that.
if #evaluation.save
#id = params[:student_id]
#id = #id.to_i + 1
You are manually calculating the next id. Ask yourself this: What happens if you are working with student.id 1, and you run this calculation, but student.id 2 has been deleted?
You get an ActiveRecord error.
You need a better way of pulling the next student. You should add an instance method to your Student model to handle that for you:
def next
Student.where(id: id).order("id ASC").first
end
In your controller you can move to the next student as such:
redirect_to evaluate_path({ student_group_id: #student_group, student_id: #student.next.id})
Then your test should be much simpler.