CSC/ECE 517 Fall 2018/E1850. Write unit tests for review response map.rb: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 15: Line 15:
==Project==
==Project==
===Problem Statement===
===Problem Statement===
[https://github.com/expertiza/expertiza/blob/master/app/models/review_response_map.rb/ review_response_map.rb] is the model file of review response in expertiza. Expertiza has function called review, used to provide suggestion to author of particular project, hence, author could give feed back to the reviewer. The review_response_map.rb is responsible for manages the data, logic and rules of review response.
[https://github.com/expertiza/expertiza/blob/master/app/models/review_response_map.rb/ 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===
===Task Statement===
Line 21: Line 21:


===Achievement===
===Achievement===
The RSpec test tests all 14 methods in model file with 26 test cases. The test covers lots of edge cases and tests real-life conditions.  
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.
Also, by using RSpec test we found several bugs in the review_response_map.rb file, and thus author could fix it.




== Rspec test code ==
=== Help Method ===
We use a lot of "let" to generate objects that stand in for real objects during a test, which is convenient in contrast to "double" method.
<pre>
<pre>
describe ReviewResponseMap do
   let(:participant) { build(:participant, id: 1, user: build(:student, name: 'no name', fullname: 'no one')) }
   let(:participant) { build(:participant, id: 1, user: build(:student, name: 'no name', fullname: 'no one')) }
   let(:participant2) { build(:participant, id: 2) }
   let(:participant2) { build(:participant, id: 2) }
Line 49: Line 49:
     allow(response).to receive(:map).and_return(review_response_map)
     allow(response).to receive(:map).and_return(review_response_map)
   end
   end
 
</pre>
  describe "#questionnaire" do
=== Test Cases ===
     context "when round is not nil" do
==== "questionnaire" Method ====
*Source code
<pre>
  def questionnaire(round = nil)
     Questionnaire.find_by(id: self.assignment.review_questionnaire_id(round))
  end
</pre>
*Test code
1. when round is not nil
<pre>
       it "returns the questionnaire in a certain round" do
       it "returns the questionnaire in a certain round" do
         allow(assignment).to receive(:review_questionnaire_id).with(1).and_return(1)
         allow(assignment).to receive(:review_questionnaire_id).with(1).and_return(1)
Line 57: Line 66:
         expect(review_response_map.questionnaire(1)).to eq(questionnaire)
         expect(review_response_map.questionnaire(1)).to eq(questionnaire)
       end
       end
    end
</pre>
    context "when round is nil" do
2. when round is nil
<pre>
       it "returns the questionnaire" do
       it "returns the questionnaire" do
         allow(assignment).to receive(:review_questionnaire_id).with(nil).and_return(1)
         allow(assignment).to receive(:review_questionnaire_id).with(nil).and_return(1)
Line 64: Line 74:
         expect(review_response_map.questionnaire).to eq(questionnaire)
         expect(review_response_map.questionnaire).to eq(questionnaire)
       end
       end
     end
</pre>
 
==== "get_title" Method ====
*Source code
<pre>
  def get_title
     "Review"
   end
   end
 
</pre>
  describe "#get_title" do
*Test code
<pre>
     it "returns 'Review'" do
     it "returns 'Review'" do
       expect(review_response_map.get_title).to eq('Review')
       expect(review_response_map.get_title).to eq('Review')
     end
     end
</pre>
==== "delete" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe "#delete" do
*Test code
<pre>
     it "deletes author feedback response records, metareview response records, and review response records" do
     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(review_response_map).to receive(:response).and_return(response)
Line 79: Line 108:
       expect(review_response_map.delete).to eq(review_response_map)
       expect(review_response_map.delete).to eq(review_response_map)
     end
     end
</pre>
==== ".export_fields" Method ====
*Source code
<pre>
  def self.export_fields(_options)
    ["contributor", "reviewed by"]
   end
   end
 
</pre>
  describe ".export_fields" do
*Test code
<pre>
     it "exports the fields of the csv file " do
     it "exports the fields of the csv file " do
       expect(ReviewResponseMap.export_fields('_options')).to eq(["contributor", "reviewed by"])
       expect(ReviewResponseMap.export_fields('_options')).to eq(["contributor", "reviewed by"])
    end
</pre>
==== ".export" Method ====
*Source code
<pre>
  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
   end
   end
 
</pre>
  describe ".export" do
*Test code
<pre>
     it "exports reviewer names and reviewee names to an array" do
     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])
       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])
       expect(ReviewResponseMap.export([], 1, '_options')).to eq([review_response_map])
    end
</pre>
==== ".import" Method ====
*Souce code
<pre>
  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
   end
   end
 
</pre>
describe ".import" do
*Test code
    context "when the user of the reviewee is nil" do
1. when the user of the reviewee is nil
<pre>
       it "raises an ArgumentError saying 'cannot find reviewee user'" do
       it "raises an ArgumentError saying 'cannot find reviewee user'" do
         hash = {reviewee: 'person1', reviewers: ['person2']}
         hash = {reviewee: 'person1', reviewers: ['person2']}
Line 101: Line 185:
         expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError)
         expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError)
       end
       end
    end
</pre>


    context "when the user of the reviewee is not nil" do
2. when the user of the reviewee is not nil
      context "when the participant of the reviewee is nil" do
2.1. when the participant of the reviewee is nil
<pre>
         it "raises an ArgumentError saying 'Reviewee user is not a participant in this assignment'" do
         it "raises an ArgumentError saying 'Reviewee user is not a participant in this assignment'" do
           hash = {reviewee: 'person1', reviewers: ['person2']}
           hash = {reviewee: 'person1', reviewers: ['person2']}
Line 112: Line 197:
           expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError)
           expect{ReviewResponseMap.import(hash, '_session', 1)}.to raise_error(ArgumentError)
         end
         end
      end
</pre>
 
2.2. when the participant of the reviewee is not nil
      context "when the participant of the reviewee is not nil" do
<pre>
         before(:each) do
         before(:each) do
           reviewee_user = double('User', id: 5, name: 'person1')
           reviewee_user = double('User', id: 5, name: 'person1')
Line 125: Line 210:
           allow(AssignmentParticipant).to receive(:where).and_return(reviewer_participant)  
           allow(AssignmentParticipant).to receive(:where).and_return(reviewer_participant)  
         end
         end
        context "when reviewee does not have a team" do
</pre>
2.2.1. when reviewee does not have a team
<pre>
           it "creates a team for reviewee and finds/creates a review response map record" do
           it "creates a team for reviewee and finds/creates a review response map record" do
             hash = {reviewee: 'person1', reviewers: ['person2']}
             hash = {reviewee: 'person1', reviewers: ['person2']}
Line 141: Line 228:
             expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2'])
             expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2'])
           end
           end
        end
</pre>


        context "when reviewee has a team" do
2.2.2. when reviewee has a team
<pre>
           it "finds/creates a review response map record" do
           it "finds/creates a review response map record" do
             hash = {reviewee: 'person1', reviewers: ['person2']}
             hash = {reviewee: 'person1', reviewers: ['person2']}
Line 152: Line 240:
             expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2'])
             expect(ReviewResponseMap.import(hash, '_session', 1)).to eq(['person2'])
           end
           end
        end
</pre>
      end
 
     end
==== "show_feedback" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe "#show_feedback" do
*Test code
    context "when there is no review responses and the response parameter is nil" do
1. when there is no review responses and the response parameter is nil
<pre>
       it "returns nil" do
       it "returns nil" do
         expect(review_response_map.show_feedback(nil)).to eq(nil)
         expect(review_response_map.show_feedback(nil)).to eq(nil)
         expect(review_response_map.show_feedback(empty_response)).to eq(nil)
         expect(review_response_map.show_feedback(empty_response)).to eq(nil)
       end
       end
    end
</pre>
 
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
    context "when there exist review responses or the response parameter is not nil" do
<pre>
      context "when author feedback response map record does not exist or there aren't corresponding responses" do
         it "returns the map variable" do
         it "returns the map variable" do
           allow(review_response_map).to receive_message_chain(:response, :any?) { true }
           allow(review_response_map).to receive_message_chain(:response, :any?) { true }
Line 174: Line 268:
           expect(review_response_map.show_feedback(response)).to eq(nil)
           expect(review_response_map.show_feedback(response)).to eq(nil)
         end
         end
      end
</pre>
    end


    context "when author feedback response map record exists and there exist corresponding responses" do
3. when author feedback response map record exists and there exist corresponding responses
<pre>
       it "returns the HTML code which displays the lastest author feedback response" do
       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(review_response_map).to receive_message_chain(:response, :any?) { true }
Line 192: Line 286:
                                                                   "<b>Additional Comment: </b></td></tr></table>")
                                                                   "<b>Additional Comment: </b></td></tr></table>")
       end
       end
</pre>
==== "metareview_response_maps" Method ====
*Source code
<pre>
  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
     end
    metareview_list
   end
   end
 
</pre>
  describe "#metareview_response_maps" do
*Test code
<pre>
     it "returns metareviews related to current review response map" do
     it "returns metareviews related to current review response map" do
       allow(Response).to receive(:where).and_return([response])
       allow(Response).to receive(:where).and_return([response])
Line 201: Line 307:
       expect(review_response_map.metareview_response_maps).to eq([metareview_response_map])
       expect(review_response_map.metareview_response_maps).to eq([metareview_response_map])
     end
     end
</pre>
==== ".get_responses_for_team_round" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe ".get_responses_for_team_round" do
*Test code
    context "when the team id is nil" do
1. when the team id is nil
<pre>
       it "returns an empty array" do
       it "returns an empty array" do
         expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
         expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
       end
       end
    end
</pre>
 
2. when the team id is not nil and when current response map does not have responses
<pre>
     before(:each) do
     before(:each) do
       team = team1
       team = team1
Line 217: Line 342:
       allow(map).to receive(:response).and_return([response])
       allow(map).to receive(:response).and_return([response])
     end
     end
 
</pre>
    context "when the team id is not nil" do
<pre>
      context "when current response map does not have responses" do
         it "returns an array with satisfied responses" do
         it "returns an array with satisfied responses" do
           # map = review_response_map
           # map = review_response_map
Line 225: Line 349:
           expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
           expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
         end
         end
      end
</pre>
    end
3. when current response map has responses and when all these responses don't belong to this round or have been submitted
 
<pre>
     before(:each) do
     before(:each) do
       allow(response).to receive(:any?).and_return(true)
       allow(response).to receive(:any?).and_return(true)
     end
     end
 
</pre>
    context "when current response map has responses" do
<pre>
      context "when all these responses don't belong to this round or have been submitted" do
         it "returns an array with satisfied responses" do
         it "returns an array with satisfied responses" do
           allow(response).to receive(:round).and_return(2)
           allow(response).to receive(:round).and_return(2)
           expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
           expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([])
         end
         end
      end
</pre>
    end


    context "when one or more responses belong to this round and haven't been submitted" do
4. when one or more responses belong to this round and haven't been submitted
<pre>
       it "returns an array with satisfied responses" do
       it "returns an array with satisfied responses" do
         allow(response).to receive(:round).and_return(1)
         allow(response).to receive(:round).and_return(1)
Line 247: Line 370:
         expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([response])
         expect(ReviewResponseMap.get_responses_for_team_round(team1, 1)).to eq([response])
       end
       end
     end
</pre>
 
==== ".final_versions_from_reviewer" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe ".final_versions_from_reviewer" do
*Test code
<pre>
     it "returns a hash with the latest version of response for each response map record and corresponding questionnaire ids" do
     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)
       review_id = double('1', to_i: 1)
Line 278: Line 410:
       expect(ReviewResponseMap.prepare_review_response(assignment, maps, review_final_versions, round)).to eq([])
       expect(ReviewResponseMap.prepare_review_response(assignment, maps, review_final_versions, round)).to eq([])
     end
     end
</pre>
==== ".review_response_report" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe ".review_response_report" do
*Test code
    context "when the review user is nil" do
1. when the review user is nil
<pre>
       it "returns sorted reviewers of a certain type of response map" do
       it "returns sorted reviewers of a certain type of response map" do
         id = double('id', id: 1)
         id = double('id', id: 1)
Line 292: Line 447:
         expect(ReviewResponseMap.review_response_report(id, assignment, type, nil)).to eq([reviewers])
         expect(ReviewResponseMap.review_response_report(id, assignment, type, nil)).to eq([reviewers])
       end
       end
    end
</pre>
 
2. when the review user is not nil
    context "when the review user is not nil" do
<pre>
       it "return reviewers users' full name" do
       it "return reviewers users' full name" do
         review_user = double('user', :[] => '1')
         review_user = double('user', :[] => '1')
Line 305: Line 460:
         expect(ReviewResponseMap.review_response_report(id, assignment, type, review_user)).to eq([reviewers])
         expect(ReviewResponseMap.review_response_report(id, assignment, type, review_user)).to eq([reviewers])
       end
       end
</pre>
==== "email" Method ====
*Source code
<pre>
  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
   end
   end
 
</pre>
  describe "#email" do
*Test code
<pre>
     it "sends emails to team members whose work has been reviewed" do
     it "sends emails to team members whose work has been reviewed" do
       user = double('user', id: 1)
       user = double('user', id: 1)
Line 319: Line 487:
       expect(review_response_map.email(defn, participant, assignment)).to eq([user])
       expect(review_response_map.email(defn, participant, assignment)).to eq([user])
     end
     end
</pre>
==== ".prepare_final_review_versions" Method ====
*Source code
<pre>
  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
   end
 
</pre>
  describe ".prepare_final_review_versions" do
*Test code
    context "when round number is not nil and is bigger than 1" do
1. when round number is not nil and is bigger than 1
<pre>
       xit "returns the final version of responses in each round" do
       xit "returns the final version of responses in each round" do
         # wrote in "final_versions_from_reviewer"
         # wrote in "final_versions_from_reviewer"
       end
       end
    end
</pre>
    context "when round number is nil or is smaller than or equal to 1" do
2. when round number is nil or is smaller than or equal to 1
<pre>
       xit "returns the final version of responses" do
       xit "returns the final version of responses" do
         # wrote in "final_versions_from_reviewer"
         # wrote in "final_versions_from_reviewer"
       end
       end
</pre>
==== ".prepare_review_response" Method ====
*Source code
<pre>
  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
     end
    review_final_versions[symbol][:response_ids] = response_ids
   end
   end
 
</pre>
  describe ".prepare_review_response" do
*Test code
    context "when the round is nil" do
1. when the round is nil
<pre>
       xit "uses :review as hash key and populate the hash with review questionnaire id and response ids" do
       xit "uses :review as hash key and populate the hash with review questionnaire id and response ids" do
         # wrote in "final_versions_from_reviewer"
         # wrote in "final_versions_from_reviewer"
       end
       end
    end
</pre>
    context "when the round is not nil" do
2. when the round is not nil
<pre>
       xit "uses review round number as hash key and populate the hash with review questionnaire id, round, and response ids" do
       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"
         # wrote in "final_versions_from_reviewer"
       end
       end
    end
  end
end
</pre>
</pre>



Revision as of 18:04, 9 November 2018

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.


Help Method

We use a lot of "let" to generate objects that stand in for real objects during a test, which is convenient in contrast to "double" method.

  let(:participant) { build(:participant, id: 1, user: build(:student, name: 'no name', fullname: 'no one')) }
  let(:participant2) { build(:participant, id: 2) }
  let(:assignment) { build(:assignment, id: 1, name: 'Test Assgt') }
  let(:team1) { build(:assignment_team) }
  let(:review_response_map) { build(:review_response_map, assignment: assignment, reviewer: participant, reviewee: team1) }
  let(:response) { build(:response, id: 1, map_id: 1, response_map: review_response_map, scores: [answer]) }
  let(:answer) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) }
  let(:answer2) { Answer.new(answer: 2, comments: 'Answer text', question_id: 2) }
  let(:question) { Criterion.new(id: 1, weight: 2, break_before: true) }
  let(:question2) { TextArea.new(id: 1, weight: 2, break_before: true) }
  let(:questionnaire) { ReviewQuestionnaire.new(id: 1, questions: [question], max_question_score: 5) }
  let(:questionnaire2) { ReviewQuestionnaire.new(id: 2, questions: [question2], max_question_score: 5) }
  let(:tag_prompt) { TagPrompt.new(id: 1, prompt: "prompt") }
  let(:tag_prompt_deployment) { TagPromptDeployment.new(id: 1, tag_prompt_id: 1, assignment_id: 1, questionnaire_id: 1, question_type: 'Criterion') }
  let(:empty_response) { build(:response, id: nil, map_id: nil, response_map: nil, scores: [answer]) }
  let(:feed_back_response_map) { double('feed_back_response_map', reviewed_object_id: 1, response: empty_response) }
  let(:metareview_response_map) { double('somemap') }

  before(:each) do
    allow(response).to receive(:map).and_return(review_response_map)
  end

Test Cases

"questionnaire" Method

  • Source code
  def questionnaire(round = nil)
    Questionnaire.find_by(id: self.assignment.review_questionnaire_id(round))
  end
  • 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
  • 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
  • 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
  • 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.

References

Expertiza documentation

Expertiza in Github

Code

Demo

Rspec Documentation