CSC/ECE 517 Fall 2018/OSS E1848 Write unit tests for assignment team.rb

From Expertiza_Wiki
Jump to navigation Jump to search

For this project, the goal is to write up unit tests for assignment_team.rb

Introduction

Assignment_team.rb in Expertiza provides a method for student to assign a team and finish one assignment. Features come at the cost of complexity; this project is focused on creating the test methods to ensure that user interaction with the assignment interface remains stable and reliable.

  • The forked git repository for this project can be found here
  • The pull request link can be found here

Bugs in assignment_team.rb

We found some bugs in assignment_team.rb:

  • In assign_reviewer(reviewer)
 42: assignment = Assignment.find(self.parent_id) => assignment = Assignment.find_by(id: parent_id)

Here, the "find(self.parent_id)" should be changed into "find_by(id: parent_id)". In the first case, if there's no instance that has this parent_id, the "find(self.parent_id)" returns an error, but in the second case, the method returns an empty instance.

When the assignment record cannot be found by the parent id of the current assignment team, we are supposed to raise a customer exception, not the exception that's returned by the find function. So we need to change the method into "find_by(id: parent_id)".

  • In self.import(row, assignment_id, options)
 108: id.to_s => assignment_id.to_s
 The id doesn't exist in the AssignmentTeam

Help Method

We use 'let' to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples.

  let(:user) { User.new(id: 1) }
  let(:assignment_team) { build(:assignment_team, id: 2, parent_id: 2, name: "team2", users: [user], submitted_hyperlinks: "https://www.1.ncsu.edu") }
  let(:assignment_team1) { build(:assignment_team, id: 1, parent_id: 1, name: "team1", submitted_hyperlinks: "") }
  let(:assignment_team2) { build(:assignment_team, id: 3, parent_id: 3, directory_num: -1) }
  let(:questions) { {QuizQuestionnaire: double(:question)} }
  let(:questionnaire) { build(:questionnaire) }
  let(:assignment) { build(:assignment, id: 1, questionnaires: [questionnaire], name: 'Test Assgt') }
  let(:courseTeam) { build(:course_team, id: 1) }
  let(:team) { build(:assignment_team) }
  let(:team_user) { build(:team_user) }
  let(:team_without_submitted_hyperlinks) { build(:assignment_team, submitted_hyperlinks: "") }
  let(:participant1) { build(:participant, id: 1, user: build(:student, id: 1, name: 'no name', fullname: 'no one')) }
  let(:participant2) { build(:participant, id: 2) }
  let(:review_response_map) { build(:review_response_map, id: 1, assignment: assignment, reviewer: participant1, reviewee: assignment_team1) }
  let(:signed_up_team) { build(:signed_up_team, id: 1, team_id: 1, is_waitlisted: 0, topic_id: 1) }

Project Description

There are some examples for how this project create test methods for assignment_team.rb

Create a team

In assignment_team.rb,there is a method return the team given the participant.

  def self.team(participant)
    return nil if participant.nil?
    team = nil
    teams_users = TeamsUser.where(user_id: participant.user_id)
    return nil unless teams_users
    teams_users.each do |teams_user|
      team = Team.find(teams_user.team_id)
      return team if team.parent_id == participant.parent_id
    end
    nil
  end

Based on the above method, the test case can be created, and there are three different possible results could be generated.

  • when the participant is nil
  it "returns nil" do
    expect(AssignmentTeam.team(participant1)).to eq(nil)
  end
  • when there are not team users records
  it "returns nil" do
    allow(TeamsUser).to receive(:where).with(user_id: 1).and_return(nil)
    expect(AssignmentTeam.team(participant1)).to eq(nil)
  end
  • when the participant is not nil and there exist team users records
  it "returns the team given the participant" do
    allow(TeamsUser).to receive(:where).with(user_id: 1).and_return([team_user])
    allow(Team).to receive(:find).with(1).and_return(team)
    expect(AssignmentTeam.team(participant1)).to eq(team)
  end

Remove team by id

The assignment_team.rb also provide a method which could remove team by id.

  def self.remove_team_by_id(id)
    old_team = AssignmentTeam.find(id)
    old_team.destroy unless old_team.nil?
  end

Based on the above code, the test case can be created:

  it "deletes a team given the team id" do
    old_team = assignment_team1
    allow(AssignmentTeam).to receive(:find).with(1).and_return(old_team)
    allow(old_team).to receive(:destroy).and_return(old_team)
    expect(AssignmentTeam.remove_team_by_id(1)).to eq(old_team)
  end

Return the topic chosen by the team

The assignment_team.rb offers a method that could return the topic chosen by the team.

  def topic
    SignedUpTeam.find_by(team_id: self.id, is_waitlisted: 0).try(:topic_id)
  end

Based on the above code, the test case can be created:

  describe "#topic" do
    it "returns the topic id chosen by this team" do
      allow(SignedUpTeam).to receive(:find_by).with(team_id:1, is_waitlisted: 0).and_return(signed_up_team1)
      expect(assignment_team1.topic).to eq(1)
    end
  end

This test examines the original codes ability to generate the topic that is chosen by the team because it first goes to the mocked instances to look for a team. In this case, to match the mock instance that has created, we look for the team that has team_id as 1, and not being waitlisted. The topic id is set to be 1 in the mock. This function returns 1, because it does return the topic id, for the team that has signed up for a topic.

Return whether the team has submission

The assignment_team.rb offers a method that could return either true or false, in terms of whether the team has submitted work or not

  def has_submissions?
    self.submitted_files.any? or self.submitted_hyperlinks.present?
  end

Based on the above code, the test case can be created, and there are three possible results:

  • when current assignment team submitted files
  it "returns true" do
    allow(assignment_team1).to receive(:submitted_files).and_return([double(:File)])
    expect(assignment_team1.has_submissions? ).to be true
  end
  • when current assignment team did not submit files but submitted hyperlinks
  it "returns true" do
    allow(assignment_team1).to receive(:submitted_hyperlinks).and_return([double(:Hyperlink)])
    expect(assignment_team1.has_submissions? ).to be true
  end
  • when current assignment team did not submit either files or hyperlinks
  it "returns false" do
    expect(assignment_team1.has_submissions? ).to be false
  end


This test can successfully test whether the team has submitted files/hyperlinks or not, because it creates a mock function, for the submitted_files function to receive a parameter - so that the team has hypothetically passed in a submitted file. The returning value being true shows that the has_submissions? test succeeded when the team receives a submitted file.

Similarly, we created a mock function for the team to receive a hyperlink using submitted_hyperlink. This second test case returning true shows the hyperlink being submitted is also detected by the function.

When there's no mock happening inside the function, no file or hyperlink will be passed into the team. So has_submissions? returns false is what's being expected.

Return participants of a team

The assignment_team.rb offers a method that could return the participants of a team.

  def participants
    users = self.users
    participants = []
    users.each do |user|
      participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: self.parent_id)
      participants << participant unless participant.nil?
    end
    participants
  end
  alias get_participants participants

Based on the above code, the test case can be created:

  describe "#participants" do
    it "returns participants of the current assignment team" do
      allow(AssignmentParticipant).to receive(:find_by).with(user_id: 1, parent_id: 2).and_return(participant2)
      expect(assignment_team.participants).to eq([participant2])
    end
  end

Return the first member of the team

The assignment_team.rb offers a method that could return the first member of the team.

  def self.get_first_member(team_id)
    find_by(id: team_id).try(:participants).try(:first)
  end

Based on the above code, the test case can be created:

  describe ".get_first_member" do
    it "returns the first participant of current assignment team" do
      allow(AssignmentTeam).to receive_message_chain(:find_by, :try, :try).with(id: 1).with(:participants).with(:first).and_return(participant1)
      expect(AssignmentTeam.get_first_member(1)).to eq(participant1)
    end
  end

Import csv file to form teams directly

The assignment_team.rb offers a method that could import csv file to form teams directly.

  def self.import(row, assignment_id, options)
    unless Assignment.find_by(id: assignment_id)
      raise ImportError, "The assignment with the id \"" + assignment_id.to_s + "\" was not found. <a href='/assignment/new'>Create</a> this assignment?"
    end
    @assignment_team = prototype
    Team.import(row, assignment_id, options, @assignment_team)
  end

Based on the above code, the test case can be created:

  • when there is no assignment with this assignment id
    context "when there is no assignment with this assignment id" do
      it "raises an ImportError" do
        allow(Assignment).to receive(:find_by).with(id: 1).and_return(nil)
        expect { AssignmentTeam.import([], 1, has_column_names: 'false') }
            .to raise_error(ImportError, "The assignment with the id \"1\" was not found. <a href='/assignment/new'>Create</a> this assignment?")
      end
    end
  • when there exists an assignment with this assignment id
    context "when there exists an assignment with this assignment id" do
      it "imports a csv file to form assignment teams" do
        allow(Assignment).to receive(:find_by).with(id: 2).and_return(double("Assignment", id: 2))
        allow(AssignmentTeam).to receive(:prototype).and_return(assignment_team)
        allow(Team).to receive(:import).with([], 2, {}, assignment_team).and_return(true)
        expect(AssignmentTeam.import([], 2, {})).to eq(true)
      end
    end

Copy the current Assignment team to the CourseTeam

The assignment_team.rb offers a method that could copy the current Assignment team to the CourseTeam.

  def copy(course_id)
    new_team = CourseTeam.create_team_and_node(course_id)
    new_team.name = name
    new_team.save
    copy_members(new_team)
  end

Based on the above code, the test case can be created:

  describe "#copy" do
    it "copies the current assignment team and team members to a new course team" do
      allow(CourseTeam).to receive(:create_team_and_node).with(1).and_return(courseTeam)
      allow(Team).to receive(:copy_members).with(courseTeam).and_return([])
      expect(assignment_team.copy(1)).to eq([])
    end
  end

Save files

The assignment_team.rb offers a method that could save files in one directory

  def files(directory)
    files_list = Dir[directory + "/*"]
    files = []

    files_list.each do |file|
      if File.directory?(file)
        dir_files = files(file)
        dir_files.each {|f| files << f }
      end
      files << file
    end
    files
  end

Based on the above code, the test case can be created:

  it "returns all files in certain directory" do
    expect(assignment_team1.files('./lib').count).to eq(10)
    expect(assignment_team1.files('./lib')).to match_array(["./lib/assets","./lib/hamer.rb",
                                                            "./lib/tasks", "./lib/tasks/background_email_reminder.rake",
                                                            "./lib/tasks/data_migrate.rake", "./lib/tasks/db_diagram.rake",
                                                            "./lib/tasks/gc4r_tasks.rake", "./lib/tasks/line_endings.rake",
                                                            "./lib/tasks/rbeautify.rake", "./lib/tasks/scrub_database.rake"])
  end

Export the fields

The assignment_team.rb offers a method that could export the fields

  def self.export_fields(options)
    fields = []
    fields.push("Team Name")
    fields.push("Team members") if options[:team_name] == "false"
    fields.push("Assignment Name")
  end

Based on the above code, the test case can be created:

  it "exports the fields of the csv file" do
    expect(AssignmentTeam.export_fields(team_name: 'false')).to eq(["Team Name", "Team members", "Assignment Name"])
  end

Return a hash of scores that the team has received for the questions

The assignment_team.rb offers a method that could copy the current Assignment team to the CourseTeam.

  def scores(questions)
    scores = {}
    scores[:team] = self # This doesn't appear to be used anywhere
    assignment.questionnaires.each do |questionnaire|
      scores[questionnaire.symbol] = {}
      scores[questionnaire.symbol][:assessments] = ReviewResponseMap.where(reviewee_id: self.id)
      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

Based on the above code, the test case can be created:

  describe "#scores" do
    it "returns a hash of scores that current assignment team has received for the questions" do
      allow(assignment_team).to receive(:assignment).and_return(assignment)
      allow(ReviewResponseMap).to receive(:where).with(reviewee_id: 2).and_return(review_response_map)
      allow(Answer).to receive(:compute_scores).with(review_response_map, questions[QuizQuestionnaire]).and_return(10)
      allow(assignment).to receive(:compute_total_score).and_return(10)
      # expect(assignment_team.scores(questions)[:QuizQuestionnaire]).equal?({assessments: review_response_map, scores: 10}).to be true
      expect(assignment_team.scores(questions)[:team]).to eq(assignment_team)
      expect(assignment_team.scores(questions)[:total_score]).to eq(10)
    end
  end

Return whether assignment team exist corresponding response maps

The assignment_team.rb offers a method that could judge whether there exist corresponding response maps.

  def received_any_peer_review?
    ResponseMap.where(reviewee_id: self.id, reviewed_object_id: self.parent_id).any?
  end

Based on the above code, the test case can be created:

  • when there exist corresponding response map
      it "returns true" do
        allow(ResponseMap).to receive(:where).with(reviewee_id: 1, reviewed_object_id: 1).and_return([double(:ResponseMap)])
        expect(assignment_team1.received_any_peer_review?).to be true
      end
  • when there does not exist corresponding response maps
      it "returns false" do
        allow(ResponseMap).to receive(:where).with(reviewee_id: 1, reviewed_object_id: 1).and_return([])
        expect(assignment_team1.received_any_peer_review?).to be false
      end

Set the directory num for this team

The assignment_team.rb offers a method that could Set the directory num for this team.

  def set_student_directory_num
    return if self.directory_num and self.directory_num >= 0
    max_num = AssignmentTeam.where(parent_id: self.parent_id).order('directory_num desc').first.directory_num
    dir_num = max_num ? max_num + 1 : 0
    self.update_attributes(directory_num: dir_num)
  end

Based on the above code, the test case can be created:

  describe "#set_student_directory_num" do
    context "when there is no directory number for the assignment team" do
      it "sets a directory number for the assignment team" do
        # allow(assignment_team1).to receive(:try).with(:directory).and_return(-1)
        allow(AssignmentTeam).to receive_message_chain(:where, :order, :first, :directory_num).and_return(4)
        expect(assignment_team2.set_student_directory_num).to eq(true)
      end
    end
  end


Result

Our test cases has coverage: 100%

There are in total 127 relevant lines, and all of them get passed.

Conclusion

The testing framework in the assignment_team_spec.rb used unit tests to test the functionality of each action in the class. The mock instances are created at the beginning of the file, so that during each test they don't need to be constructed again. In order to test each unit case without depending on other functionalities, the mocked actions, as well as the desired returns are built inside different test cases, depending on the need of the case.

In building the test framework, the key is to understand the input, output and the desired action of each function that we want to test. The Rspec test format also has a steep learning curve, but the test shows stable and robust result on the assignment_team.rb.