CSC/ECE 517 Fall 2018- Project E1848: Writing unit tests for assignment team.rb

From Expertiza_Wiki
Jump to navigation Jump to search

The purpose of this project is to improve the coverage and quality of the unit tests for the assignment team model in Expertiza. This project comprised the program 3 assignment of CSC/ECE 517 in fall 2018 and is entitled "Project E1848: Writing unit tests for assignment_team.rb." The source code for this project is in pull request #1228 of the Expertiza project on GitHub.

Key Contributions

Out of the 30 methods defined for the model AssignmentTeam.rb, test case was written for only one. Our contribution was to successfully add test cases for all remaining 29 methods.

Hence, increasing the coverage from 5% to 100%.

The Class under Test

AssignmentTeam
This class handles all functionalities for a team linked to an assignment. Some of the functionalities include, get a list of participants in the team, get the team name, team score for the given assignment, submissions for this team (files) and (hyperlinks) and topic picked by the team. Add/or remove participants from the team. It also checks for any reviews the team may have gotten for their work. Export all teams in a CSV file, import files from CSV to form teams.

Test Plan

Test Design

Test Structure

Listed below are the functionalities and the Rspec unit tests corresponding to the function names along with a list of scenarios tested.

Function name: includes?(participant)

This function in assignment_team.rb checks if the participant passed as argument is a member of the team or not. For this, we utilize factorybot to build a user and a participant1 as follows-:

  let(:user1) { build(:student, id: 2) }
  let(:participant1) { build(:participant, id: 1) }

Test cases checked for are-:
1) when the team receives a user, that user is allowed to become a team participant. The method returns True for the given participant passed as argument.

    context "when an assignment team has one participant" do
      it "includes one participant" do
        allow(team).to receive(:users).with(no_args).and_return([user1])
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(participant1)
        expect(team.includes?(participant1)).to eq true
      end
    end

2) when the team receives a user with no arguments, that user is not allowed to become a team participant. The method returns False for the given participant passed as argument.

    context "when an assignment team has no users" do
      it "includes no participants" do
        allow(team).to receive(:users).with(no_args).and_return([])
        expect(team.includes?(participant1)).to eq false
      end
    end

Function name: parent_model

This function in assignment_team.rb always provides the name of the parent model which is Assignment.

Hence, the unit test also always checks if the response of this method is the string "Assignment" or not.

    it "provides the name of the parent model" do
      expect(team.parent_model).to eq "Assignment"
    end

Function name: self.parent_model(id)

This function in assignment_team.rb takes in as argument id of the current Assignment for the team and returns an instance of the parent model.

Hence the unit test checks if the instance returned by this method for a given assignment id, matches the assignment or not.

    it "provides the instance of the parent model" do
      allow(Assignment).to receive(:find).with(1).and_return(assignment)
      expect(AssignmentTeam.parent_model(1)).to eq assignment
    end

Function name: fullname

This function in assignment_team.rb print out the name for the current class instance. for this we first build an assignment team instance and assign it the name "abcd".

Hence the unit test checks if the assignment team has a name, and returns that same name.

    context "when the team has a name" do
      it "provides the name of the class" do
        team = build(:assignment_team, id: 1, name: "abcd")
        expect(team.fullname).to eq "abcd"
      end
    end

Function name: review_map_type

This function in assignment_team.rb always provides the name of the review map type which is ReviewResponseMap.

Hence, the unit test also always checks if the response of this method is the string "ReviewResponseMap" or not.

    it "provides the review map type" do
      expect(team.review_map_type).to eq "ReviewResponseMap"
    end

Function name: self.prototype

This function in assignment_team.rb implements the prototype pattern by returning a new instance of the class AssignmentTeam.

The unit test checks if the response is a new instance of the class or not.

    it "provides the instance of the AssignmentTeam" do
      expect(AssignmentTeam).to receive(:new).with(no_args)
      AssignmentTeam.prototype
    end

Function name: assign_reviewer(reviewer)

Function name: reviewed_by?(reviewer)

Function name: topic

This function in assignment_team.rb return the id of the topic picked by the team for the assignment.

The unit test first stubs a topic for the given assignment, allows a SignedUpTeam object to receive that topic and AssignmentTeam id, and return the topic id to check if it matches with the topic id of the AssignmentTeam.

context "when the team has picked a topic" do
    it "provides the topic id" do
      assignment = team.assignment
      allow(SignUpTopic).to receive(:find_by).with(assignment: assignment).and_return(topic)
      allow(SignedUpTeam).to receive_message_chain(:find_by, :try).with(team_id: team.id).with(:topic_id).and_return(topic.id)
      expect(team.topic).to eq(topic.id)
    end
  end

Function name: has_submissions?

this function in assignment_team.rb is used to check if the team has submitted their work or not.

The unit test allows the team to receive submissions, by first receiving files as submission, then hyperlink as submission, and for each expects the the team's submission to be true when the function is called.

    context "when a team has submitted files" do
      it "has submissions" do
        allow(team).to receive_message_chain(:submitted_files, :any?).with(no_args).with(no_args).and_return(true)
      end
    end

    context "when the team has submitted hyperlink" do
      it "checks if the team has submissions" do
        allow(team).to receive_message_chain(:submitted_hyperlinks, :present?).with(no_args).with(no_args).and_return(true)
      end
    end

    after(:each) do
      expect(team.has_submissions?).to be true
    end

Function name: participants

This function in assignment_team.rb returns a list of all participants in the team.

The unit test first allows the team to receive two user instances and then adds these users as participants of the team. The test then expects the function to return a list of these two users as participants of the team.

    context "when an assignment team has two participants" do
      it "has those two participants" do
        allow(team).to receive(:users).with(no_args).and_return([user1, user2])
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(participant1)
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user2.id, parent_id: team.parent_id).and_return(participant2)
        expect(team.participants).to eq [participant1, participant2]
      end
    end

The unit test first allows the team to receive a user instance but does not add this user as participant of the team. The test then expects the function to return an empty list as participants of the team.

    context "when an assignment team has a user but no participants" do
      it "includes no participants" do
        allow(team).to receive(:users).with(no_args).and_return([user1])
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(nil)
        expect(team.participants).to eq []
      end
    end

Function name: delete

This function in assignment_team.rb deletes the AssignmentTeam by destroying each participant in the team and returns an instance of AssignmentTeam.

The unit test first allows two users to be added as team participants. The test checks if the team deleted matches the current team instance.

    it "deletes the team" do
      allow(team).to receive(:users).with(no_args).and_return([user1, user2])
      allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(participant1)
      allow(AssignmentParticipant).to receive(:find_by).with(user_id: user2.id, parent_id: team.parent_id).and_return(participant2)
      expect(team.delete).to eq(team)
    end

Function name: destroy

Function name: self.get_first_member(team_id)

This function in assignment_team.rb returns the instance of the first participant of the team for the given team id.

The unit test allows the assignment to receive a user, add it as a participant and return the first participant in the team. The test expects the function to return an instance of this participant.

    context "when team id is present" do
      it "get first member of the  team" do
        allow(AssignmentTeam).to receive_message_chain(:find_by, :try, :try).with(id: team.id).with(:participant).with(:first).and_return(participant1)
        expect(AssignmentTeam.get_first_member(team.id)).to eq(participant1)
      end
    end

Function name: submitted_files(path = self.path)

Function name: self.import(row, assignment_id, options)

Function name: self.export(csv, parent_id, options)

Function name: copy(course_id)

This function in assignment_team.rb copies the current assignment team and creates a new CourseTeam instance from the given team. It takes as parameter the id of the course assignment team has.

The test grabs the course id from the assignment team and calls the function to check if a course team is successfully created or not. The function returns and empty array, otherwise returns nil if the course team is not created successfully.

    context "for given assignment team" do
      it "copies the assignment team to course team" do
        assignment = team.assignment
        course = assignment.course
        expect(team.copy(course.id)).to eq([])
      end
    end

Function name: add_participant(assignment_id, user)

This function in assignment_team.rb adds the user to the AssignmentTeam for the given assignment.

The unit test creates a user, and passes the user, assignment id for the team to the function and expects the result to be an instance of AssignmentParticipant meaning the user was successfully added as a participant.

    context "when a user is not a part of the team" do
      it "adds the user to the team" do
        user = build(:student, id: 10)
        assignment = team.assignment
        expect(team.add_participant(assignment.id, user)).to be_an_instance_of(AssignmentParticipant)
      end
    end


The unit test creates a user, adds him as a participant to the team, and passes the user, assignment id for the team to the function and expects the result to be nil as the user already exists as participant of the team.

    context "when a user is already a part of the team" do
      it "returns without adding user to the team" do
        allow(team).to receive(:users).with(no_args).and_return([user1])
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(participant1)
        assignment = team.assignment
        expect(team.add_participant(assignment.id, user1)).to eq(nil)
      end
    end

Function name: scores(questions)

Function name: files(directory)

This method in assignment_team.rb successfully returns a list of files in any given directory path, not just the assignment teams directory path.

The unit test assigns the directory path as given below and checks if the list of files returned contains a specific file which is known to be in the list.

    context "when file is present in the directory" do
      it "provides the list of files in directory and checks if file is present" do
        directory = "spec/models"
        expect(team.files(directory)).to include("spec/models/assignment_team_spec.rb")
      end
    end

The unit test assigns the directory path as given below and checks if the list of files returned does not contain a specific file which is known to not be in the list.

    context "when file is not present in the directory" do
      it "provides the list of files in directory and checks if file is not present" do
        directory = "spec/controllers"
        expect(team.files(directory)).not_to include("spec/models/assignment_team_spec.rb")
      end
    end

Function name: submit_hyperlink(hyperlink)

Function name: remove_hyperlink(hyperlink_to_delete)

Function name: self.team(participant)

This function in assignment_team.rb returns the AssingmentTeam object that the given participant belongs to.

The unit test first builds a user and assigns that user as a participant1 of the team. Then allows that participant to be added as a team user and finally search the team for the given team user and expect the team for participant1 to be equal to the AssignmentTeam.

    context "when there is a participant" do
      it "provides the team for participant" do
        teamuser = build(:team_user, id: 1, team_id: team.id, user_id: user1.id)
        allow(team).to receive(:users).with(no_args).and_return([user1])
        allow(AssignmentParticipant).to receive(:find_by).with(user_id: user1.id, parent_id: team.parent_id).and_return(participant1)
        allow(TeamsUser).to receive(:where).with(user_id: participant1.user_id).and_return([teamuser])
        allow(Team).to receive(:find).with(teamuser.team_id).and_return(team)
        expect(AssignmentTeam.team(participant1)).to eq(team)
      end
    end

Function name: self.export_fields(options)

This function in assignment_team.rb exports the fields for a given set of options. It always returns a list containing ["Team Name", "Assignment Name"] if the given set of options have a team name set to true.

The unit test expects the method to return a static list of strings for the team passed as options, with the team having a name.

    context "when team has name" do
      it "exports the fields" do
        expect(AssignmentTeam.export_fields(team)).to eq(["Team Name", "Assignment Name"])
      end
    end

Function name: self.remove_team_by_id(id)

Function name: path

Function name: set_student_directory_num

Function name: received_any_peer_review?

Test Coverage

The model has a total of 237 lines, out of which 127 were relevant lines. We achieved 100% coverage with avg hits per line at 2.3 (the number of times each specific line was run during the test suite)

Stubs for Isolating the UUT

DRY Testing Practices

Factories and the let RSPEC Helper Method

Contexts

Testing Framework

RSpec Introduction

RSPEC is a testing framework for Ruby for behavior-driven development (BDD) licensed under MIT. It is inspired by JBehave and contains fully integrated JMock based framework which has a very rich and powerful DSL (domain-specific language) which resembles a natural language specification. Composed of multiple libraries structured to work together, RSpec provides encapsulated testing via the describe block to specify the behavior of the class and the context for the unit test case.

Why RSpec?

RSpec is easy to learn and implement and can be used with other testing tools like Cucumber and Minitest independently. It is extremely powerful for testing states with complicated setup and also helps in tearing down complex code to access the objects required for testing RSpec semantics encourage agile thinking and practice and it structures the tests in a more intuitive way.

Bugs Fixed

During testing of the AssignmentTeam model, we found two bugs, one in AssignmentTeam#submit_hyperlink and one in AssignmentTeam.import. In AssignmentTeam#submit_hyperlink, http:// was appended to hyperlink strings without this tag or https:// at the start of them, rather than prepended. We fixed this error in our pull request. In AssignmentTeam.import, id, an undefined parameter, was referenced while raising an exception, and we updated it to assignment_id, which was the actual name of the parameter passed to the method.

Results

Future Work

DRY principle could be cleaned up a little more. Sometimes it became necessary to use the let or build helper methods in each unit test. They may all be consolidated at the beginning.
Some more edge cases could be thought of for each method, and tests written for those. Although, we have tried to cover as many as possible.
Additionally, in the future, the code may also be refactored with a more efficient tool.
Test coverage through quality tests consistently added throughout the development of the Expertiza project should be the future work of the project.

References

1. Expertiza
2. Expertiza Wikipedia
3. Expertiza Github Repository
4. RSpec - Relish
5. Engineering Software as a Service