CSC/ECE 517 Fall 2018 - Project E1852 Write unit tests for participant.rb

From Expertiza_Wiki
Revision as of 01:41, 10 November 2018 by Xji7 (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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


Introduction

The objective for this project is to write unit tests using Rspec for participant.rb model, which is used to prepare data for participants enrolled in each course/assignment. By editing the participant_spct.rb, all class and instance methods are tested, and the path coverage is above 90% with all 97 lines covered.

Expertiza

Expertiza is an open source educational web application developed on Ruby on Rails framework. Using Expertiza, students can submit and peer-review learning objects such as articles, code, and websites. The source code is available on Github and it allows students to improve and maintain.

Behavior-DrivenDevelopment

In software engineering, behavior-driven development (BDD) is a software development process that emerged from test-driven development(TDD). The behavior-driven development combines the general techniques and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and management teams with shared tools and a shared process to collaborate on software development.

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-score, rspec-expectation and rspec-mock.

Problem Statement

The initial unit tests’ path coverage is only 36.08% with 35 lines covered and 62 lines missed for participant.rb, which are not enough. The unit test should be improved by making the path coverage of more than 90% and achieve the highest possible branch coverage.

Files Involved

A test file and two model files were modified for this project:

spec/models/participant_spec.rb

app/models/participant.rb

app/models/assignment_participant.rb

Test Plan

Expertiza Environment Setup

1. Install Virtual Box software form Oracle in PC

2. Download the Ubuntu-Expertiza image

3. Run the .ova file in Virtual Box

4. Run following commands in terminal

git clone http://github.com/user_name/expertiza

cd expertiza

./setup.sh

bundle install

rake db:migrate

rails s

5. Open up the browser and put localhost:3000 in the address bar. The expertiza login page should appear


Understand the Functionality

participant_spec.rb

participant_spec.rb is the file that test should be written in.

participant.rb

participant.rb is used to preparing data for participants enrolled in each course/assignment. It allows the user to review some information relate to the current course/assignment and execute operations such as sorting participant by their names and removing a current participant from the team. It also helps determine users' permission or authorization.

assignment_participant.rb

assignment_participant.rb is a model file related to participant.rb. AssignmentParticipant is a subclass of Participant class, and it overrides some methods of its superclass, which affects the process of writing test code.


Analyze Problems and Solutions

1. The email method in app/models/participant.rb uses "assignment_id" which is not correct and causes the test cannot pass. After changed "assignment_id" to "assignment.id", the problem fixed.

2. Since AssignmentParticipant is a subclass of Participant class, and it overrides team method and score method of its superclass, the test cannot access the methods in the superclass. As a result, those two methods were removed to make the test pass.


Write Test Code

mock instance

All of the mock instances are listed at the beginning of the test file.

  let(:topic1) { build(:topic, topic_name: '') }
  let(:topic2) { build(:topic, topic_name: "topic_name") }
  let(:student) { build(:student, name: "John Smith", email: "sjohn@ncsu.edu", fullname: "Mr.John Smith") }
  let(:student1) { build(:student, name: "Alice") }
  let(:student2) { build(:student, name: "Bob") }
  let(:assignment) { build(:assignment, name: "assignment") }
  let(:participant) { build(:participant, user: student, assignment: assignment, can_review: false, handle: "nb") }
  let(:participant1) { build(:participant, user: student1) }
  let(:participant2) { build(:participant, user: student2) }
  let(:assignment_team) { build(:assignment_team, name: "ChinaNo1") }
  let(:team_user) { build(:team_user, team: assignment_team, user: student) }
  let(:response) { build(:response, response_map: response_map) }
  let(:response_map) { build(:review_response_map, assignment: assignment, reviewer: participant1, reviewee: assignment_team) }
  let(:questions) { build(:question) }
  let(:questionnaire) { build(:questionnaire) }

team

This case tests the team name of the current user shows up when team method is called.

  describe "#team" do
    it "returns the first team of current user" do
      allow(TeamsUser).to receive(:find_by).with(user: student).and_return(team_user)
      expect(participant.team.name).to eq "ChinaNo1"
    end
  end

responses

This case tests that all of the responses of the current user received show up when responses method is called.

  describe "#responses" do
    it "returns all responses this participant received" do
      allow(participant).to receive(:response).and_return(response)
      expect(participant.responses).to eq []
    end
  end

name

This case is given by the initial test file.

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

fullname

This case is given by the initial test file.

describe "#fullname" do
    it "returns the full name of the user" do
      expect(participant.fullname).to eq "Mr.John Smith"
    end
  end

able_to_review

This case tests that the permission of whether the current user can review others' work should be returned when able_to_reveiw method is called.

  describe "#able_to_review" do
    it "returns whether the current participant has permission to review others' work" do
      expect(participant.able_to_review).to be false
    end
  end

email

This case tests that an email about assignment registration will be sent to the current user when email method is called.

  describe "#email" do
    it "sends an assignment registration email to current user" do
      allow(User).to receive(:find_by).with(id: nil).and_return(student)
      allow(Assignment).to receive(:find_by).with(id: nil).and_return(assignment)
      expect(participant.email(12_345, 'homepage').subject).to eq "You have been registered as a participant in the Assignment assignment"
      expect(participant.email(12_345, 'homepage').to[0]).to eq "expertiza.development@gmail.com"
      expect(participant.email(12_345, 'homepage').from[0]).to eq "expertiza.development@gmail.com"
    end
  end

topic_name

Two cases are designed for testing this method. When the topic_name method is called, the dash (-) should show up when there is no such topic or the topic name is empty, or the topic name should appear.

    context "when the topic is nil or the topic name is empty" do
      it "returns em dash (—)" do
        expect(participant.topic_name).to eq "<center>—</center>"
      end
    end
    context "when the topic is not nil and the topic name is not empty" do
      it "returns the topic name" do
        allow(participant).to receive(:topic).and_return(topic2)
        expect(participant.topic_name).to eq "topic_name"
      end
    end

sort_by_name

This case tests that a list of users' names should show up alphabetical when email method is called.

  describe ".sort_by_name" do
    it "returns sorted participants based on their user names" do
      expect(Participant.sort_by_name([participant1, participant2, participant]).collect {|p| p.user.name }).to eq ["Alice", "Bob", "John Smith"]
    end
  end

scores

Two cases are designed for testing this method.

    context "when the round is nil" do
      it "uses questionnaire symbol as a hash key and populates the score hash" do
        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(assignment).to receive(:questionnaires).and_return([questionnaire])
        allow(questionnaire).to receive(:get_assessments_for).with(participant).and_return(response)
        allow(Answer).to receive(:compute_scores).and_return(max: 10, min: 10, avg: 10)
        allow(assignment).to receive(:compute_total_score).with(any_args).and_return(10)
        expect(participant.scores(questions).inspect).to eq("{:participant=>#<AssignmentParticipant id: nil, can_submit: true, "\
        "can_review: false, user_id: nil, parent_id: nil, submitted_at: nil, permission_granted: nil, penalty_accumulated: 0, grade: nil, "\
        "type: \"AssignmentParticipant\", handle: \"nb\", 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=>10, :min=>10, :avg=>10}}, :total_score=>10}")
      end
    end
    context "when the round is not nil" do
      it "uses questionnaire symbol with round as hash key and populates the score hash" do
        allow(AssignmentQuestionnaire).to receive_message_chain(:find_by, :used_in_round).with(assignment_id: 1, questionnaire_id: 2)\
          .with(no_args).and_return(3)
        allow(assignment).to receive(:questionnaires).and_return([questionnaire])
        allow(questionnaire).to receive(:get_assessments_for).with(participant).and_return(response)
        allow(Answer).to receive(:compute_scores).and_return(max: 10, min: 10, avg: 10)
        allow(assignment).to receive(:compute_total_score).with(any_args).and_return(10)
        expect(participant.scores(questions).inspect).to eq("{:participant=>#<AssignmentParticipant id: nil, can_submit: true, "\
        "can_review: false, user_id: nil, parent_id: nil, submitted_at: nil, permission_granted: nil, penalty_accumulated: 0, grade: nil, "\
        "type: \"AssignmentParticipant\", handle: \"nb\", time_stamp: nil, digital_signature: nil, duty: nil, can_take_quiz: true, Hamer: 1.0, "\
        "Lauw: 0.0>, :review3=>{: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=>10, :min=>10, :avg=>10}}, :total_score=>10}")
      end
    end

get_permissions

Four cases are designed for testing this method. The authorization of the current user is identified before returning the permissions this user has.

    context "when the current user is a participant" do
      it "returns a hash with value {can_submit: true, can_review: true, can_take_quiz: true}" do
        expect(Participant.get_permissions("participant")).to eq(can_submit: true, can_review: true, can_take_quiz: true)
      end
    end
    context "when the current user is a reader" do
      it "returns a hash with value {can_submit: false, can_review: true, can_take_quiz: true}" do
        expect(Participant.get_permissions("reader")).to eq(can_submit: false, can_review: true, can_take_quiz: true)
      end
    end
    context "when the current user is a submitter" do
      it "returns a hash with value {can_submit: true, can_review: false, can_take_quiz: false}" do
        expect(Participant.get_permissions("submitter")).to eq(can_submit: true, can_review: false, can_take_quiz: false)
      end
    end
    context "when the current user is a reviewer" do
      it "returns a hash with value {can_submit: false, can_review: true, can_take_quiz: false}" do
        expect(Participant.get_permissions("reviewer")).to eq(can_submit: false, can_review: true, can_take_quiz: false)
      end
    end

get_authorization

This test is a reverse version of get_permissions.

    context " when the current user is able to submit work, review others' work and take quizzes" do
      it "indicates the current user is a participant" do
        expect(Participant.get_authorization(true, true, true)).to eq "participant"
      end
    end
    context " when the current user is unable to submit work but is able to review others' work and take quizzes" do
      it "indicates the current user is a reader" do
        expect(Participant.get_authorization(false, true, true)).to eq "reader"
      end
    end
    context " when the current user is able to submit work but is unable to review others' work and take quizzes" do
      it "indicates the current user is a submitter" do
        expect(Participant.get_authorization(true, false, false)).to eq "submitter"
      end
    end
    context " when the current user is unable to submit work and take quizzes but is able to review others' work" do
      it "indicates the current user is a reviewer" do
        expect(Participant.get_authorization(false, true, false)).to eq "reviewer"
      end
    end

handle

Two cases are designed for testing this method. When the handle method is called, 'handle' should be returned if the anonymized view is turned on, or the handle of the current participant should be returned

    context "when the anonymized view is turn on" do
      it "always returns 'handle'" do
        allow(User).to receive(:anonymized_view?).with(nil).and_return(true)
        expect(participant.handle).to eq('handle')
      end
    end
    context "when the anonymized view is turn off" do
      it "returns the handle of current participant" do
        allow(User).to receive(:anonymized_view?).with(nil).and_return(false)
        expect(participant.handle).to eq("nb")
      end
    end

delete

Three cases are designed for testing this method. When the delete method is called, it will check whether it is a force delete first. If it is a force delete, current participant, related response maps, teams, and team users will be deleted. Or it will check whether there are associations to determine whether the operation can be executed or not.

    context "when force deleting the response maps related to current participant" do
      it "force deletes current participant, related response maps, teams, and team users" do
        allow(ResponseMap).to receive(:where).with("reviewee_id = ? or reviewer_id = ?", nil, nil).and_return([response_map])
        expect(participant.delete(true).handle).to eq 'nb'
      end
    end
    context "when not force deleting the response maps related to current participant" do
      context "when there are no related response maps to this participant and current participant did not join any teams" do
        it "force deletes current participant, related response maps, teams, and team users" do
          allow(ResponseMap).to receive(:where).with("reviewee_id = ? or reviewer_id = ?", nil, nil).and_return([])
          allow(participant).to receive(:team).and_return(nil)
          expect(participant.delete(false).handle).to eq 'nb'
        end
      end
      context "when there are some related response maps to this participant or current participant join one or more teams" do
        it "raises an exception saying 'Associations exist for this participant'" do
          allow(participant).to receive(:team).and_return(assignment_team)
          expect { participant.delete(false) }.to raise_error("Associations exist for this participant.")
        end
      end
    end

force_delete

Three cases are designed for testing this method. When the force_delete method is called, the current participant, as well as the team, should be deleted when the user is the only member. Or the other teammate should be removed from that team. If the current participant does not have a team, it should be deleted directly.

    context "when the current participant has already joined a team" do
      context "when the current participant is the only member in that team" do
        it "deletes that team and current participant" do
          allow(participant).to receive(:team).and_return(assignment_team)
          allow(participant).to receive_message_chain(:team, :teams_users, :length).and_return(1)
          expect(participant.force_delete([response_map]).handle).to eq 'nb'
        end
      end
      context "when the team has other team members" do
        it "removes the current participant from that team and deletes current participant" do
          allow(participant).to receive(:team).and_return(assignment_team)
          allow(participant).to receive_message_chain(:team, :teams_users, :length).and_return(3)
          allow(participant).to receive_message_chain(:team, :teams_users).and_return([participant, participant1, participant2])
          expect(participant.force_delete([response_map]).handle).to eq 'nb'
        end
      end
    end
    context "when the current participant did not join a team" do
      it "deletes current participant directly" do
        allow(participant).to receive(:team).and_return(nil)
        expect(participant.force_delete([response_map]).handle).to eq 'nb'
      end
    end
  end

Test Result

With all class and instance methods tested, the path coverage is 100% with all 97 lines covered.

Full video of running the test can be found at https://youtu.be/mZoskFhmMWM.

Reference

Behavior-DrivenDevelopment https://en.wikipedia.org/wiki/Behavior-driven_development

Expertiza https://expertiza.ncsu.edu/

Rspec Documentation https://relishapp.com/rspec

Github (Expertiza) https://github.com/expertiza/expertiza