CSC/ECE 517 Fall 2018/E1850. Write unit tests for review response map.rb
Introduction
Background
Expertiza
Expertiza is an open source web based peer review system developed and maintained by students and faculty members at North Carolina State University. It enables students enrolled in a particular course to form online teams and complete assignments.
RSpec
RSpec is a 'Domain Specific Language' (DSL) testing tool written in Ruby to test Ruby code. It is a behavior-driven development (BDD) framework which is extensively used in the production applications. The basic idea behind this concept is that of Test Driven Development (TDD) where the tests are written first and the development is based on writing just enough code that will fulfill those tests followed by refactoring. It contains its own mocking framework that is fully integrated into the framework based upon JMock. The simplicity in the RSpec syntax makes it one of the popular testing tools for Ruby applications. The RSpec tool can be used by installing the rspec gem which consists of 3 other gems namely rspec-core, rspec-expectation and rspec-mock.
Team
Zhewei Hu (zhu6) (mentor)
- Cheng Yuan (cyuan7)
- Yue Tian (ytian22)
- Ke Zhao (kzhao2)
Project
Problem Statement
review_response_map.rb is the model file of review response in Expertiza. Expertiza has a function called review, used to provide suggestion to the author of a particular project, hence, the author could give feedback to the reviewer. The review_response_map.rb is responsible for manages the data, logic, and rules of review response.
Task Statement
The reveiw_response_map.rb did not has test file. We write RSpec test file review_response_map_spec.rb which tested if the model file run all function properly.
Achievement
The RSpec test tests all 14 methods in the model file with 26 test cases. The test covers lots of edge cases and tests real-life conditions. Also, by using RSpec test we found several bugs in the review_response_map.rb file, and thus author could fix it.
Test Cases
"questionnaire" Method
- Source code
def questionnaire(round = nil) Questionnaire.find_by(id: self.assignment.review_questionnaire_id(round)) end
- Process
This is to find certain questionnnaire. Help method: let(:assignment) { build(:assignment, id: 1, name: 'Test Assgt') }
- Test code
1. when round is not nil
it "returns the questionnaire in a certain round" do allow(assignment).to receive(:review_questionnaire_id).with(1).and_return(1) allow(Questionnaire).to receive(:find_by).with(id: 1).and_return(questionnaire) expect(review_response_map.questionnaire(1)).to eq(questionnaire) end
2. when round is nil
it "returns the questionnaire" do allow(assignment).to receive(:review_questionnaire_id).with(nil).and_return(1) allow(Questionnaire).to receive(:find_by).with(id: 1).and_return(questionnaire) expect(review_response_map.questionnaire).to eq(questionnaire) end
"get_title" Method
- Source code
def get_title "Review" end
- Test code
it "returns 'Review'" do expect(review_response_map.get_title).to eq('Review') end
"delete" Method
- Source code
def delete(_force = nil) fmaps = FeedbackResponseMap.where(reviewed_object_id: self.response.response_id) fmaps.each(&:destroy) maps = MetareviewResponseMap.where(reviewed_object_id: self.id) maps.each(&:destroy) self.destroy end
- Process
This is to delete certain record. When delete, this function will return the record.
- Test code
it "deletes author feedback response records, metareview response records, and review response records" do allow(review_response_map).to receive(:response).and_return(response) allow(response).to receive(:response_id).and_return(1) expect(review_response_map.delete).to eq(review_response_map) end
".export_fields" Method
- Source code
def self.export_fields(_options) ["contributor", "reviewed by"] end
- Test code
it "exports the fields of the csv file " do expect(ReviewResponseMap.export_fields('_options')).to eq(["contributor", "reviewed by"]) end
".export" Method
- Source code
def self.export(csv, parent_id, _options) mappings = where(reviewed_object_id: parent_id).to_a mappings.sort! {|a, b| a.reviewee.name <=> b.reviewee.name } mappings.each do |map| csv << [ map.reviewee.name, map.reviewer.name ] end end
- Process
This is to export records to csv files. One review_response_map is a record.
- Test code
it "exports reviewer names and reviewee names to an array" do allow(ReviewResponseMap).to receive(:where).with(reviewed_object_id: 1).and_return([review_response_map]) expect(ReviewResponseMap.export([], 1, '_options')).to eq([review_response_map]) end
".import" Method
- Souce code
def self.import(row_hash, _session, assignment_id) reviewee_user_name = row_hash[:reviewee].to_s reviewee_user = User.find_by(name: reviewee_user_name) raise ArgumentError, "Cannot find reviewee user." unless reviewee_user reviewee_participant = AssignmentParticipant.find_by(user_id: reviewee_user.id, parent_id: assignment_id) raise ArgumentError, "Reviewee user is not a participant in this assignment." unless reviewee_participant reviewee_team = AssignmentTeam.team(reviewee_participant) if reviewee_team.nil? # lazy team creation: if the reviewee does not have team, create one. reviewee_team = AssignmentTeam.create(name: 'Team' + '_' + rand(1000).to_s, parent_id: assignment_id, type: 'AssignmentTeam') t_user = TeamsUser.create(team_id: reviewee_team.id, user_id: reviewee_user.id) team_node = TeamNode.create(parent_id: assignment_id, node_object_id: reviewee_team.id) TeamUserNode.create(parent_id: team_node.id, node_object_id: t_user.id) end row_hash[:reviewers].each do |reviewer| reviewer_user_name = reviewer.to_s reviewer_user = User.find_by(name: reviewer_user_name) raise ArgumentError, "Cannot find reviewer user." unless reviewer_user next if reviewer_user_name.empty? reviewer_participant = AssignmentParticipant.find_by(user_id: reviewer_user.id, parent_id: assignment_id) raise ArgumentError, "Reviewer user is not a participant in this assignment." unless reviewer_participant ReviewResponseMap.find_or_create_by(reviewed_object_id: assignment_id, reviewer_id: reviewer_participant.id, reviewee_id: reviewee_team.id, calibrate_to: false) end end
- Process
This is to import certain record. We use a hash {reviewee: 'person1', reviewers: ['person2']} to do the test. When user or participant of reviewee is nil, ArgumentError will be raised. When they are not nil and team exists, we use reviewee_team, reviwee_user, reviewer_user, reviewee_participant, reviewer_participant to test the method. When the team doesn not exist, we need to first use TeamUser, TeamNode and TeamUserNode to create a reviewee_team.
- Test code
1. when the user of the reviewee is nil
it "raises an ArgumentError saying 'cannot find reviewee user'" do hash = {reviewee: 'person1', reviewers: ['person2']} allow(User).to receive(:find_by).and_return(nil) expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError) end
2. when the user of the reviewee is not nil 2.1. when the participant of the reviewee is nil
it "raises an ArgumentError saying 'Reviewee user is not a participant in this assignment'" do hash = {reviewee: 'person1', reviewers: ['person2']} reviewee_user = double('User', id: 5, name: 'person1') allow(User).to receive(:find_by).with(name: 'person1').and_return(reviewee_user) allow(AssignmentParticipant).to receive(:find_by).and_return(nil) expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError) end
2.2. when the participant of the reviewee is not nil
before(:each) do reviewee_user = double('User', id: 5, name: 'person1') allow(User).to receive(:find_by).with(name: 'person1').and_return(reviewee_user) reviewee_participant = double('AssignmentParticipant', user_id: 5, parent_id: 1, id: 3) allow(AssignmentParticipant).to receive(:find_by).and_return(reviewee_participant) reviewer_user = double('User', id: 6, name: 'person2') allow(User).to receive(:find_by).with(name: 'person2').and_return(reviewer_user) reviewer_participant = double('AssignmentParticipant', user_id: 6, parent_id: 1, id: 4) allow(AssignmentParticipant).to receive(:where).and_return(reviewer_participant) end
2.2.1. when reviewee does not have a team
it "creates a team for reviewee and finds/creates a review response map record" do hash = {reviewee: 'person1', reviewers: ['person2']} allow(AssignmentTeam).to receive(:team).and_return(nil) reviewee_team = double('AssignmentTeam', name: 'Team_1', parent_id: 1, id: 2) allow(AssignmentTeam).to receive(:create).and_return(reviewee_team) t_user = double('TeamUser', team_id: 2, user_id: 5, id: 7) allow(TeamsUser).to receive(:create).and_return(t_user) team_node = double('TeamNode', parent_id: 1, node_object_id: 2, id: 4) allow(TeamNode).to receive(:create).and_return(team_node) team_user_node = double('TeamUserNode', parent_id: 4, node_object_id: 7) allow(TeamUserNode).to receive(:create).and_return(team_user_node) review_response_map1 = double('ReviewResponseMap', reviewed_object_id: 1, reviewer_id: 4, reviewee_id: 2, calibrate_to: false) allow(ReviewResponseMap).to receive(:find_by).and_return(review_response_map1) expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2']) end
2.2.2. when reviewee has a team
it "finds/creates a review response map record" do hash = {reviewee: 'person1', reviewers: ['person2']} reviewee_team = double('AssignmentTeam', parent_id: 1, id: 2) allow(AssignmentTeam).to receive(:team).and_return(reviewee_team) review_response_map1 = double('ReviewResponseMap', reviewed_object_id: 1, reviewer_id: 4, reviewee_id: 2, calibrate_to: false) allow(ReviewResponseMap).to receive(:find_by).and_return(review_response_map1) expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2']) end
"show_feedback" Method
- Source code
def show_feedback(response) return unless self.response.any? and response map = FeedbackResponseMap.find_by(reviewed_object_id: response.id) return map.response.last.display_as_html if map and map.response.any? end
- Test code
1. when there is no review responses and the response parameter is nil
it "returns nil" do expect(review_response_map.show_feedback(nil)).to eq(nil) expect(review_response_map.show_feedback(empty_response)).to eq(nil) end
2. when there exist review responses or the response parameter is not nil and when author feedback response map record does not exist or there aren't corresponding responses
it "returns the map variable" do allow(review_response_map).to receive_message_chain(:response, :any?) { true } allow(FeedbackResponseMap).to receive(:find_by).and_return(feed_back_response_map) map = feed_back_response_map allow(map).to receive_message_chain(:response, :any?) { false } expect(review_response_map.show_feedback(response)).to eq(nil) end
3. when author feedback response map record exists and there exist corresponding responses
it "returns the HTML code which displays the lastest author feedback response" do allow(review_response_map).to receive_message_chain(:response, :any?) { true } allow(FeedbackResponseMap).to receive(:find_by).and_return(feed_back_response_map) map = feed_back_response_map allow(map).to receive_message_chain(:response, :any?) { true } allow(map).to receive_message_chain(:response, :last).and_return(response) expect(review_response_map.show_feedback(response)).to eq("<table width=\"100%\"><tr>" \ "<td align=\"left\" width=\"70%\"><b>Review </b>" \ " <a href=\"#\" name= \"review_1Link\" onClick=\"toggleElement('review_1','review');" \ "return false;\">show review</a></td><td align=\"left\"><b>Last Reviewed:</b>" \ "<span>Not available</span></td></tr></table>" \ "<table id=\"review_1\" style=\"display: none;\" class=\"table table-bordered\"><tr><td>" \ "<b>Additional Comment: </b></td></tr></table>") end
"metareview_response_maps" Method
- Source code
def metareview_response_maps responses = Response.where(map_id: self.id) metareview_list = [] responses.each do |response| metareview_response_maps = MetareviewResponseMap.where(reviewed_object_id: response.id) metareview_response_maps.each {|metareview_response_map| metareview_list << metareview_response_map } end metareview_list end
- Test code
it "returns metareviews related to current review response map" do allow(Response).to receive(:where).and_return([response]) allow(MetareviewResponseMap).to receive(:where).with(reviewed_object_id: 1).and_return([metareview_response_map]) expect(review_response_map.metareview_response_maps).to eq([metareview_response_map]) end
".get_responses_for_team_round" Method
- Source code
def self.get_responses_for_team_round(team, round) responses = [] if team.id maps = ResponseMap.where(reviewee_id: team.id, type: "ReviewResponseMap") maps.each do |map| if map.response.any? and map.response.reject {|r| (r.round != round || !r.is_submitted) }.any? responses << map.response.reject {|r| (r.round != round || !r.is_submitted) }.last end end responses.sort! {|a, b| a.map.reviewer.fullname <=> b.map.reviewer.fullname } end responses end
- Test code
1. when the team id is nil
it "returns an empty array" do expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([]) end
2. when the team id is not nil and when current response map does not have responses
before(:each) do team = team1 allow(team).to receive(:id).and_return(1) allow(ResponseMap).to receive(:where).and_return([review_response_map]) map = review_response_map allow(map).to receive(:response).and_return([response]) end
it "returns an array with satisfied responses" do # map = review_response_map allow(response).to receive(:any?).and_return(false) expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([]) end
3. when current response map has responses and when all these responses don't belong to this round or have been submitted
before(:each) do allow(response).to receive(:any?).and_return(true) end
it "returns an array with satisfied responses" do allow(response).to receive(:round).and_return(2) expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([]) end
4. when one or more responses belong to this round and haven't been submitted
it "returns an array with satisfied responses" do allow(response).to receive(:round).and_return(1) allow(response).to receive(:is_submitted).and_return(true) expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([response]) end
".final_versions_from_reviewer" Method
- Source code
def self.final_versions_from_reviewer(reviewer_id) maps = ReviewResponseMap.where(reviewer_id: reviewer_id) assignment = Assignment.find(Participant.find(reviewer_id).parent_id) prepare_final_review_versions(assignment, maps) end
- Test code
it "returns a hash with the latest version of response for each response map record and corresponding questionnaire ids" do review_id = double('1', to_i: 1) maps = [] allow(ReviewResponseMap).to receive(:where).and_return(maps) allow(Assignment).to receive(:find).and_return(assignment) allow(Participant).to receive_message_chain(:find, :parent_id).and_return(participant) expect(ReviewResponseMap.final_versions_from_reviewer(review_id)).to eq(ReviewResponseMap.prepare_final_review_versions(assignment, maps)) maps = [] review_final_versions = {} round = 2 allow(assignment).to receive(:rounds_of_reviews).and_return(round) expect(ReviewResponseMap.prepare_final_review_versions(assignment, maps)).to \ eq({:"review round1"=>{:questionnaire_id=>nil, :response_ids=>[]}, :"review round2"=>{:questionnaire_id=>nil, :response_ids=>[]}}) round = nil assignment = double('assignment', round_of_reviews: 3, review_questionnaire_id: 1) allow(assignment).to receive(:rounds_of_reviews).and_return(round) expect(ReviewResponseMap.prepare_final_review_versions(assignment, maps)).to \ eq({:review=>{:questionnaire_id=>1, :response_ids=>[]}}) allow(assignment).to receive(:review_questionnaire_id).and_return(1) map = double('map', id: 1) maps = [map] responses = [] round = 1 allow(Response).to receive(:where).and_return([]) allow(responses).to receive_message_chain(:last, :id).and_return([]) expect(ReviewResponseMap.prepare_review_response(assignment, maps, review_final_versions, round)).to eq([]) end
".review_response_report" Method
- Source code
def self.review_response_report(id, assignment, type, review_user) if review_user.nil? # This is not a search, so find all reviewers for this assignment response_maps_with_distinct_participant_id = ResponseMap.select("DISTINCT reviewer_id").where('reviewed_object_id = ? and type = ? and calibrate_to = ?', id, type, 0) @reviewers = [] response_maps_with_distinct_participant_id.each do |reviewer_id_from_response_map| @reviewers << AssignmentParticipant.find(reviewer_id_from_response_map.reviewer_id) end @reviewers = Participant.sort_by_name(@reviewers) else # This is a search, so find reviewers by user's full name user_ids = User.select("DISTINCT id").where('fullname LIKE ?', '%' + review_user[:fullname] + '%') @reviewers = AssignmentParticipant.where('user_id IN (?) and parent_id = ?', user_ids, assignment.id) end # @review_scores[reveiwer_id][reviewee_id] = score for assignments not using vary_rubric_by_rounds feature # @review_scores[reviewer_id][round][reviewee_id] = score for assignments using vary_rubric_by_rounds feature end
- Test code
1. when the review user is nil
it "returns sorted reviewers of a certain type of response map" do id = double('id', id: 1) type = double('type', type: 'type') reviewers = double('reviewers') allow(ResponseMap).to receive_message_chain(:select, :where).and_return([review_response_map]) # allow(ResponseMap).to receive(:each).and_return(1) allow(AssignmentParticipant).to receive(:find).and_return([reviewers]) allow(Participant).to receive(:sort_by_name).and_return([reviewers]) expect(ReviewResponseMap.review_response_report(id, assignment, type, nil)).to eq([reviewers]) end
2. when the review user is not nil
it "return reviewers users' full name" do review_user = double('user', :[] => '1') user_ids = double('user_ids') id = double('id', id: 1) type = double('type', type: 'type') reviewers = double('reviewers', fullname: 'zhaoke') allow(User).to receive_message_chain(:select, :where).and_return([user_ids]) allow(AssignmentParticipant).to receive(:where).and_return([reviewers]) expect(ReviewResponseMap.review_response_report(id, assignment, type, review_user)).to eq([reviewers]) end
"email" Method
- Source code
def email(defn, _participant, assignment) defn[:body][:type] = "Peer Review" AssignmentTeam.find(reviewee_id).users.each do |user| defn[:body][:obj_name] = assignment.name defn[:body][:first_name] = User.find(user.id).fullname defn[:to] = User.find(user.id).email Mailer.sync_message(defn).deliver_now end end
- Test code
it "sends emails to team members whose work has been reviewed" do user = double('user', id: 1) defn = {body: {type: "peer review", obj_name: "name1", first_name: "fname", partial_name: "name2"}, to: "email1"} allow(AssignmentTeam).to receive_message_chain(:find, :users).and_return([user]) allow(assignment).to receive(:name).and_return('') allow(User).to receive_message_chain(:find, :fullname).and_return('') allow(User).to receive_message_chain(:find, :email).and_return('') allow(Mailer).to receive_message_chain(:sync_message, :deliver_now).and_return('') expect(review_response_map.email(defn, participant, assignment)).to eq([user]) end
".prepare_final_review_versions" Method
- Source code
def self.prepare_final_review_versions(assignment, maps) review_final_versions = {} rounds_num = assignment.rounds_of_reviews if rounds_num and rounds_num > 1 (1..rounds_num).each do |round| prepare_review_response(assignment, maps, review_final_versions, round) end else prepare_review_response(assignment, maps, review_final_versions, nil) end review_final_versions end
- Test code
1. when round number is not nil and is bigger than 1
xit "returns the final version of responses in each round" do # wrote in "final_versions_from_reviewer" end
2. when round number is nil or is smaller than or equal to 1
xit "returns the final version of responses" do # wrote in "final_versions_from_reviewer" end
".prepare_review_response" Method
- Source code
def self.prepare_review_response(assignment, maps, review_final_versions, round) symbol = if round.nil? :review else ("review round" + round.to_s).to_sym end review_final_versions[symbol] = {} review_final_versions[symbol][:questionnaire_id] = assignment.review_questionnaire_id(round) response_ids = [] maps.each do |map| where_map = {map_id: map.id} where_map[:round] = round unless round.nil? responses = Response.where(where_map) response_ids << responses.last.id unless responses.empty? end review_final_versions[symbol][:response_ids] = response_ids end
- Test code
1. when the round is nil
xit "uses :review as hash key and populate the hash with review questionnaire id and response ids" do # wrote in "final_versions_from_reviewer" end
2. when the round is not nil
xit "uses review round number as hash key and populate the hash with review questionnaire id, round, and response ids" do # wrote in "final_versions_from_reviewer" end
Run & Result
The tests can be run on the terminal from inside the expertiza folder using following command:
rspec spec/models/review_response_map_spec.rb
The test coverage is 100% in 102 relevant lines.