CSC/ECE 517 Fall 2018/E1852 Unit Test for Participant Model

From Expertiza_Wiki
(Redirected from User talk:Rshakya)
Jump to navigation Jump to search

This wiki page describes the changes made under E1852 OSS Assignment for Fall 2018, CSC/ECE 517.



Expertiza

Expertiza is a web application designed for academia. The users of this application are instructors and students. A user as an instructor can create new assignments as well as customize existing assignments for students. Students can submit assignments, view assignment deadlines, bid for an assignment topic, create a team and peer-review learning objects (articles, code, websites, etc).


Problem Statement

The Rspec file participant_spec.rb existed with test cases for name and fullname methods and provided path coverage of 36.08% for the participant model. The test cases must be written so that the path coverage is above 90%. Also, ensure that the branch coverage is as high as possible for many edge cases.


Files Involved

The files to be worked upon are:

1. app/models/participant.rb

2. spec/models/participant_spec.rb


Team Members

Students who collaborated to work on this problem statement are :

1. Reetu Shakya (rshakya@ncsu.edu)

2. Suhas Naramballi Gururaja (snaramb@ncsu.edu)

3. Tejas Gupta (tgupta4@ncsu.edu)


Steps for Implementation

Plan of Work

The task is to write unit test cases for testing the participant model file. The procedure for project planning included :-

1. Setting the Expertiza environment. We used NCSU VCL image of [CSC517, S18] Ruby on Rails/Expertiza.

2. Understand the functionality of model file in participant.rb

3. Understand the linked data attributes being used, like assignment, topic, questionnaire

4. Writing testing conditions for different functions.


Setting the Expertiza environment

1. Reserve a NCSU VCL image of [CSC517, S18] Ruby on Rails / Expertiza.

2. Connect to the reservation using SSH and enter your campus credentials for User ID and password.

3. Commands executed for setup in terminal of VCL image:

  sudo su
  gem install bundler
  exit
  git clone [Your forked Expertiza repo url]
  cd expertiza
  bash setup.sh
  (change config/database.yml, there is no MySQL password by default)
  bundle install
  rails server


Unit Testing with RSpec

Expertiza is an open source project written in Ruby, hence it uses Rspec which is a unit test framework for the Ruby programming language. To access the effectiveness of our testing, we have used SimpleCov, a code coverage analysis tool.


Test Cases

Test responses

A Participant has many ResponseMaps that map a connection between this participant as a reviewer and as a reviewee. Each ResponseMap has many Responses associated with it. When responses method is called on a Participant, it should return an array of responses associated to this Participant.

   def responses
    response_maps.map(&:response)
   end

The test below will test a participant with no corresponding mapping. Hence the result of the responses provided by the participant is a nil list

 describe '#responses' do
     it 'returns an array of responses of the participant' do
       expect(participant.responses).to eql([])
     end
end


Test name

The method name returns the name of the participants

 def name(ip_address = nil)
    self.user.name(ip_address)
end

The test below will return the name of the participant. A factory build initializes the participant with the name as student and calls the function to expect student as the outcome

 describe "#name" do
    it "returns the name of the user" do
      expect(participant.name).to eq "Student"
    end
end


A Participant is a User. When name method is called on a Participant, it should return the name of this User. Link title

Test fullname

A Participant is a User. When fullname method is called on a Participant, it should return the full name of this User.

  def fullname(ip_address = nil)
    self.user.fullname(ip_address)
end

The test is done similar to name that it expects a name which was loaded in the factory build

describe "#name" do
    it "returns the name of the user" do
      expect(participant.name).to eq "Student"
    end
end


Test delete and Forced delete

The method deletes a participant team if the participant is not associated with any team. It makes a call to force delete when the argument is true A single test case can validate the positive scenario of delete method

  def delete(force = nil)
    maps = ResponseMap.where('reviewee_id = ? or reviewer_id = ?', self.id, self.id)
    if force or (maps.blank? and self.team.nil?)
      force_delete(maps)
    else
      raise "Associations exist for this participant."
    end
end
 
 def force_delete(maps)
    maps and maps.each(&:destroy)
    if self.team and self.team.teams_users.length == 1
      self.team.delete
    elsif self.team
      self.team.teams_users.each {|teams_user| teams_user.destroy if teams_user.user_id == self.id }
    end
    self.destroy
end

The test case will attempt to delete the participant. Since the participant is not having association it will call the forced delete function and returns participant for deleting.

describe "#delete" do
    it " should remove a participant if there is no pre-existing association" do
      expect(participant.delete(true)).to eq(participant)
    end
end

Test topic_name

The method return the name of the topic which the participant has been assigned with.

 
  def topic_name
    if topic.nil? or topic.topic_name.empty?
      "<center>—</center>" # em dash
    else
      topic.topic_name
    end
end

The test will check for the error when the participant is having an assignment without a topic, unnamed topic and with a topic.

describe '#topic_name' do     
     context 'when the participant has an assignment without a topic' do
       it 'returns error message' do
         expect(participant.topic_name).to eql('<center>—</center>')
       end
     end
     
     context 'when the participant has an assignment with an unnamed topic' do
       it 'returns error message' do
         allow(participant2).to receive(:topic).and_return(topic1)
         expect(participant2.topic_name).to eql('<center>—</center>')
       end
     end
     
     context 'when the participant has an assignment with a named topic' do
       it 'returns the name of the topic associated to the assignment of the participant' do
         allow(participant2).to receive(:topic).and_return(topic2)
         expect(topic2.topic_name).to eql('Test topic name')
         expect(participant2.topic_name).to eql('Test topic name')
       end
     end   
   end


Test able_to_review A simple method which gives the status on whether participant can review.

 def able_to_review
   can_review
 end

When able_to_review method is called on a Participant, it should return true if it can review and false otherwise.

 describe '#able_to_review' do
     it 'returns true if the participant can review' do
       expect(participant.able_to_review).to eql(true)
     end
end


When scores method is called on a Participant, it should return the total scores it received for a given number of questions.

Test get_permissions

This method returns a hash of the boolean value based on the authorization. The result of the boolean determined the ability to review , take quiz and submit.

def self.get_permissions(authorization)
    can_submit = true
    can_review = true
    can_take_quiz = true
    case authorization
    when 'reader'
      can_submit = false
    when 'reviewer'
      can_submit = false
      can_take_quiz = false
    when 'submitter'
      can_review = false
      can_take_quiz = false
    end
    {can_submit: can_submit, can_review: can_review, can_take_quiz: can_take_quiz}
end

The test case here checks whether the output hash will have the correct combination of trues and false for the authorization

  describe '.get_permissions' do
     context 'when current participant is authorized as reader' do
       it 'participant cannot submit' do
         expect(Participant.get_permissions('reader')).to eql({ :can_submit => false, :can_review => true, :can_take_quiz => true})
       end
     end

     context 'when current participant is authorized as reviewer' do
       it 'participant can only review' do
         expect(Participant.get_permissions('reviewer')).to eql({ :can_submit => false, :can_review => true, :can_take_quiz => false})
       end
     end

     context 'when current participant is authorized as submitter' do
       it 'participant can only submit' do
         expect(Participant.get_permissions('submitter')).to eql({ :can_submit => true, :can_review => false, :can_take_quiz => false})
       end
     end  
end

Test get_authorization

The method get authorization basically determies the role based on the ability to submit, review and take quiz

  def self.get_authorization(can_submit, can_review, can_take_quiz)
    authorization = 'participant'
    authorization = 'reader' if can_submit == false and can_review == true and can_take_quiz == true
    authorization = 'submitter' if can_submit == true and can_review == false and can_take_quiz == false
    authorization = 'reviewer' if can_submit == false and can_review == true and can_take_quiz == false
    authorization
end

The test cases below are designed to validate whether the method produced the 3 desired outcomes.

The first test case validates the outcome to be reader, second one check for the outcome submitter and third will check if its a reviewer

 describe '.get_authorization'  do
    context 'when input is true for can_review and can_take quiz' do
      it 'returns authorization as reader' do
        expect(Participant.get_authorization(false, true, true )).to eq('reader')
      end
    end
    
    context 'when input is true only for can_submit' do
      it 'returns authorization submitter' do
        expect(Participant.get_authorization(true, false, false)).to eq('submitter')
      end
    end
    
    context 'when the input is true for only for can_review' do
      it 'returns authorization as reviewer' do
        expect(Participant.get_authorization(false, true, false)).to eq('reviewer')
      end
    end
   end

Test sort_by_name

Participants are Users. When self.sort_by_name method is called on a Participant, it should sort a given set of participants based on their user names.

def self.sort_by_name(participants)
    users = []
    participants.each {|p| users << p.user }
    users.sort! {|a, b| a.name.downcase <=> b.name.downcase } # Sort the users based on the name
    participants.sort_by {|p| users.map(&:id).index(p.user_id) }
end

The test builds participants using factory methods and assigns name as specified. Then a function calls for sorting and the expected outcome is matched with the result

 
describe '.sort_by_name' do
     it 'sorts a set of participants based on their usernames' do
       expect(Participant.sort_by_name([participant, participant3, participant2])).to match_array([participant, participant2, participant3])
       
       names = []
       sorted_participants = Participant.sort_by_name([participant, participant3, participant2])
       expect(sorted_participants.length).to eql(3)
       sorted_participants.each do |p|
         names << p.user.name
       end
       expect(names).to match_array(["Student", "Student2", "Student3"]) 
     end  
   end

Test Scores

The method scores calculates the score based on the number of the round.

  def scores(questions)
    scores = {}
    scores[:participant] = self
    self.assignment.questionnaires.each do |questionnaire|
      round = AssignmentQuestionnaire.find_by(assignment_id: self.assignment.id, questionnaire_id: questionnaire.id).used_in_round
      questionnaire_symbol = if round
                               (questionnaire.symbol.to_s + round.to_s).to_sym
                             else
                               questionnaire.symbol
                             end
      scores[questionnaire_symbol] = {}
      scores[questionnaire_symbol][:assessments] = questionnaire.get_assessments_for(self)
      scores[questionnaire_symbol][:scores] = Answer.compute_scores(scores[questionnaire_symbol][:assessments], questions[questionnaire_symbol])
    end
    scores[:total_score] = assignment.compute_total_score(scores)
    scores
end


The first condition is when the round is nil

  describe '#scores' do
     context 'when the round is nil' do
       it 'returns scores obtained by the participant for given questions' do
         allow(assignment).to receive(:questionnaires).and_return([review_questionnaire])
         allow(AssignmentQuestionnaire).to receive_message_chain(:find_by, :used_in_round)\
         .with(assignment_id: 1, questionnaire_id: 2).with(no_args).and_return(nil)
         allow(review_questionnaire).to receive(:get_assessments_for).with(participant2).and_return([response])
         allow(Answer).to receive(:compute_scores).and_return(max: 95, min: 88, avg: 90)
         allow(assignment).to receive(:compute_total_score).with(any_args).and_return(100)
         expect(participant2.assignment.compute_total_score(:test)).to eql(100)
         #puts participant2.scores(question)
         expect(participant2.scores(question)).to include(:total_score=>100)
         expect(participant2.scores(question).inspect).to eq("{:participant=>#<AssignmentParticipant id: nil, can_submit: true, "\
         "can_review: true, user_id: nil, parent_id: nil, submitted_at: nil, permission_granted: nil, penalty_accumulated: 0, "\
         "grade: nil, type: \"AssignmentParticipant\", handle: \"handle\", time_stamp: nil, digital_signature: nil, duty: nil, "\
         "can_take_quiz: true, Hamer: 1.0, Lauw: 0.0>, :review=>{:assessments=>[#<Response id: nil, map_id: nil, additional_comment: nil,"\
         " created_at: nil, updated_at: nil, version_num: nil, round: 1, is_submitted: false>], :scores=>{:max=>95, :min=>88, :avg=>90}}, "\
         ":total_score=>100}")
       end
end

The second condition is when it is not nil


context 'when the round is not nil' do
       it 'returns scores obtained by the participant for given questions' do
         allow(assignment).to receive(:questionnaires).and_return([review_questionnaire])
         allow(AssignmentQuestionnaire).to receive_message_chain(:find_by, :used_in_round)\
         .with(assignment_id: 1, questionnaire_id: 2).with(no_args).and_return(2)
         allow(review_questionnaire).to receive(:get_assessments_for).with(participant2).and_return([response])
         allow(Answer).to receive(:compute_scores).and_return(max: 95, min: 88, avg: 90)
         allow(assignment).to receive(:compute_total_score).with(any_args).and_return(100)
         expect(participant2.assignment.compute_total_score(:test)).to eql(100)
         #puts participant2.scores(question)
         expect(participant2.scores(question).inspect).to eq("{:participant=>#<AssignmentParticipant id: nil, can_submit: true, "\
         "can_review: true, user_id: nil, parent_id: nil, submitted_at: nil, permission_granted: nil, penalty_accumulated: 0, "\
         "grade: nil, type: \"AssignmentParticipant\", handle: \"handle\", time_stamp: nil, digital_signature: nil, duty: nil, "\
         "can_take_quiz: true, Hamer: 1.0, Lauw: 0.0>, :review2=>{:assessments=>[#<Response id: nil, map_id: nil, additional_comment: nil,"\
         " created_at: nil, updated_at: nil, version_num: nil, round: 1, is_submitted: false>], :scores=>{:max=>95, :min=>88, :avg=>90}},"\
         " :total_score=>100}")
       end
end

Test Email The method here sends a email to the participant with the appropriate message,

  def email(pw, home_page)
    user = User.find_by(id: self.user_id)
    assignment = Assignment.find_by(id: self.assignment_id)

    Mailer.sync_message(
      recipients: user.email,
      subject: "You have been registered as a participant in the Assignment #{assignment.name}",
      body: {
        home_page: home_page,
        first_name: ApplicationHelper.get_user_first_name(user),
        name: user.name,
        password: pw,
        partial_name: "register"
      }
    ).deliver
end

To test this we are going to create mock objects to emulate the User, participant and assignment models to return the required objects.
Then we are going to call the function to send the email to the subject and also we are going to validate if the email ID is correct.
  describe '#email' do
    it 'sends email to the current user' do
      allow(User).to receive(:find_by).with(id: nil).and_return(student)
      allow(participant).to receive(:assignment_id).and_return(nil)
      allow(Assignment).to receive(:find_by).with(id: nil).and_return(assignment)
      expect(participant.email('password', 'home_page').subject).to eq("You have been registered as a participant in the Assignment final2")
      expect(participant.email('password', 'home_page').to[0]).to eq ("expertiza.development@gmail.com")
    end
end

Results

We have a coverage of 94.05%. To achieve the coverage for scores(questions) method in participant.rb, we had to comment out the overriding method scores(questions) in its sub-class assignment_participant.rb.

The test running can be seen at OSS Project: E1852 Unit Test for Participant Model The repository having the modified model files can be viewed at http://github.com/Suhas-NG/expertiza/blob/master/spec/models/participant_spec.rb

References

  1. Expertiza on GitHub
  2. GitHub Project Repository Fork
  3. The live Expertiza website
  4. Rspec Documentation