CSC/ECE 517 Fall 2016/E1658. Refractor lottery controller.rb and write integration tests

From Expertiza_Wiki
Jump to navigation Jump to search

Expertiza Background

Expertiza is an educational web application created and maintained by the joint efforts of the students and the faculty at NCSU. It’s an open source project developed on Ruby on Rails platform and it’s code is available on Github. It allows students to review each other’s work and improve their work upon this feedback.

Description of Current Project

The following is an Expertiza based OSS project which deals primarily with the lottery_controller.rb file. It focusses on refactoring some of the more complex methods, modifying some of the language to make it more Ruby friendly, removing some redundant code. The goal of this project is to attempt to make this part of the application easier to read and maintain. The lottery_controller contains all of the logic for running the lottery. Code climate is used in this project to detect various issues such as high cyclomatic complexity, bad naming, etc.

Changes needed

Fix following issues mentioned below:

  • Cyclomatic complexity for run_intelligent_bid is too high.
  • Perceived complexity for run_intelligent_bid is too high.
  • Use snake_case for variable names.
  • Avoid more than 3 levels of block nesting.

Changes performed

The code was refactored and its Code Climate rating improved from F to A.

Before Refractoring:


After Refractoring:

  • Cyclomatic complexity for run_intelligent_bid and other methods is greatly reduced.
  • Perceived complexity for run_intelligent_bid is also reduced.
  • snake_case are used for variable names.
  • More than 3 levels of block nesting is avoided.

Before:

          if bid_record.nil?
          bids << 0
          else
          bids << bid_record.priority ||= 0
          end

After:

 bids << bid_record.nil? ? 0 : bid_record.priority ||= 0

Assignment using if condition was changed to conditional operator


Before:

assignment = Assignment.find_by_id(params[:id])

After:

assignment = Assignment.find_by(id: params[:id])

Changed to find_by from find_by_id


Before:

response = RestClient.post url, data.to_json, :content_type => :json, :accept => :json

After:

response = RestClient.post url, data.to_json, content_type: :json, accept: :json

Changed to the latest ruby convention for HashMaps


Before:

def create_new_teams_for_bidding_response(teams, assignment)
    teams.each_with_index do |user_ids, index|
      new_team = AssignmentTeam.create(name: assignment.name + '_Team' + rand(1000).to_s, 
                                       parent_id: assignment.id, 
                                       type: 'AssignmentTeam')
      parent = TeamNode.create(parent_id: assignment.id, node_object_id: new_team.id)
      user_ids.each do |user_id|
        team_user = TeamsUser.where(user_id: user_id, team_id: new_team.id).first rescue nil
        team_user = TeamsUser.create(user_id: user_id, team_id: new_team.id) if team_user.nil?
        TeamUserNode.create(parent_id: parent.id, node_object_id: team_user.id) 
      end
    end
  end


After:

def create_new_teams_for_bidding_response(teams, assignment)
    teams.each_with_index do |user_ids|
      new_team = AssignmentTeam.create(name: assignment.name + '_Team' + rand(1000).to_s,
                                       parent_id: assignment.id,
                                       type: 'AssignmentTeam')
      parent = TeamNode.create(parent_id: assignment.id, node_object_id: new_team.id)
      user_ids.each do |user_id|
        team_user = TeamsUser.where(user_id: user_id, team_id: new_team.id).first rescue nil
        team_user = TeamsUser.create(user_id: user_id, team_id: new_team.id) if team_user.nil?
        TeamUserNode.create(parent_id: parent.id, node_object_id: team_user.id)
      end
 end

Removed unused variables


Before:

unless Assignment.find_by_id(params[:id]).is_intelligent # if the assignment is intelligent then redirect to the tree display list
      flash[:error] = "This action not allowed. The assignment " + Assignment.find_by_id(params[:id]).name + " does not enabled intelligent assignments."

After:

unless Assignment.find_by(id: params[:id]).is_intelligent # if the assignment is intelligent then redirect to the tree display list
      flash[:error] = "This action not allowed. The assignment " + Assignment.find_by(id: params[:id]).name + " does not enabled intelligent assignments."

Changed find_by_id to find_by


Before:

  unassignedTeams = AssignmentTeam.where(parent_id: params[:id]).reject {|t| !SignedUpTeam.where(team_id: t.id).empty?}
    unassignedTeams.sort! {|t1, t2| TeamsUser.where(team_id: t2.id).size <=> TeamsUser.where(team_id: t1.id).size}
    team_bids = []
    unassignedTeams.each do |team|
      topic_bids = []
      sign_up_topics.each do |topic|
        student_bids = []
        TeamsUser.where(team_id: team.id).each do |s|
          student_bid = Bid.where(user_id: s.user_id, topic_id: topic.id).first rescue nil
          if !student_bid.nil? and !student_bid.priority.nil?
            student_bids << student_bid.priority
          end
        end
        #takes the most frequent priority as the team priority
        freq = student_bids.inject(Hash.new(0)) { |h,v| h[v] += 1; h}
        topic_bids << {topic_id: topic.id, priority: student_bids.max_by { |v| freq[v] }} unless freq.empty?
      end
      topic_bids.sort! {|b| b[:priority]}
      team_bids << {team_id: team.id, bids: topic_bids}
 end

After:

 unassigned_teams = AssignmentTeam.where(parent_id: params[:id]).reject {|t| !SignedUpTeam.where(team_id: t.id).empty? }
    unassigned_teams.sort! {|t1, t2| TeamsUser.where(team_id: t2.id).size <=> TeamsUser.where(team_id: t1.id).size }
    team_bids = []
    unassigned_teams.each do |team|
      topic_bids = []
      sign_up_topics.each do |topic|
        student_bids = []
        TeamsUser.where(team_id: team.id).each do |s|
          student_bid = Bid.where(user_id: s.user_id, topic_id: topic.id).first rescue nil
          if !student_bid.nil? 
            if !student_bid.priority.nil?
            student_bids << student_bid.priority
          end
        end
        # takes the most frequent priority as the team priority
        freq = student_bids.each_with_object(Hash.new(0)) do |v, h|
          h[v] += 1
        end
        topic_bids << {topic_id: topic.id, priority: student_bids.max_by {|v| freq[v] }} unless freq.empty?
      end
      topic_bids.sort! {|b| b[:priority] }
      team_bids << {team_id: team.id, bids: topic_bids}
 end

Used snake case naming convention. Removed and condition inside if and converted it to nested if condition check. Replaced inject with each_with_obj.


Before:

def auto_merge_teams(unassignedTeams, _finalTeamTopics)
    assignment = Assignment.find(params[:id])
    # Sort unassigned
    unassignedTeams = Team.where(id: unassignedTeams).sort_by {|t| !t.users.size }
    unassignedTeams.each do |team|
      sortedBids = Bid.where(user_id: team.id).sort_by(&:priority) # Get priority for each unassignmed team
      sortedBids.each do |b|
        # SignedUpTeam.where(:topic=>b.topic_id).first.team_id
        winningTeam = SignedUpTeam.where(topic: b.topic_id).first.team_id
        next unless TeamsUser.where(team_id: winningTeam).size + team.users.size <= assignment.max_team_size # If the team can be merged to a bigger team
        TeamsUser.where(team_id: team.id).update_all(team_id: winningTeam)
        Bid.delete_all(user_id: team.id)
        Team.delete(team.id)
        break
      end
    end
  end

After:

before_action :fetch_assignment, only: [:auto_merge_teams]
  before_action :sort_unassigned_teams, only: [:auto_merge_teams]
  def auto_merge_teams(unassigned_teams, _final_team_topics)  
    # Sort unassigned
    
    unassigned_teams.each do |team|
      sorted_bids = Bid.where(user_id: team.id).sort_by(&:priority) # Get priority for each unassigned team
      sorted_bids.each do |b|
        # SignedUpTeam.where(:topic=>b.topic_id).first.team_id
        winning_team = SignedUpTeam.where(topic: b.topic_id).first.team_id
        next unless TeamsUser.where(team_id: winning_team).size + team.users.size <= assignment.max_team_size # If the team can be merged to a bigger team
        TeamsUser.where(team_id: team.id).update_all(team_id: winning_team)
        Bid.delete_all(user_id: team.id)
        Team.delete(team.id)
        break
      end
    end
  end
  def fetch_assignment
    assignment = Assignment.find(params[:id])
  end  
  
  def sort_unassigned_teams
    unassigned_teams = Team.where(id: unassigned_teams).sort_by {|t| !t.users.size }
  end  
end

Split the function into smaller pieces to keep up with coding conventions.

Test Cases

Initially, the lottery controller had only one test case: for the action run_intelligent_bid. We added one more test case for run_intelligent_bid and also added test cases for all the other actions in the controller.

1. For the action run_intelligent_assignmnent, we have two test cases:

  • webservice call should be successful

This is to check if the web service is getting called. But the URL given in the code is currently is of a web service that is currently down. Hence for testing purposes we have used a dummy URL. This can be replaced with the correct URL to the web service when it becomes available in the future.

  • should return json response

This test case checks if the response from the web service is in JSON format or not.


2. For the action run_intelligent_bid, we have the following test cases:

  • should do intelligent assignment

This was an already existing test case. It tests whether the assignment received is intelligent or not.

  • should exit gracefully when assignment not intelligent

This checks if the control redirects to main page in case the assignment received is not intelligent.

3. For the action create_new_teams_for_bidding_response, we have the following test cases:

  • should create team and return teamid

This test checks if the method is creating the team for bidding by taking in the assignment as parameter.

4. For the action auto_merge_teams, we have written the following test cases:

  • sorts the unassigned teams

This test case checks whether the method is able to sort teams which have not been assigned to any assignments yet.


Conclusions

As a result of our work, the code quality of the controller lottery_controller.rb has improved a lot. Code Climate has given it the highest possible rating. The original code had a code climate rating of F. We worked on all the issues pointed out by code climate and and improved the rating to A.
Initially, there was only one test case and only one of the methods in the controller was covered. We have also added RSpec test cases for all the methods in the the controller and hence the test coverage has increased.
This was a very good learning experience. We learnt how to write test cases in RSpec and got a chance to contribute to Open Source Software. Another major take away from the project was the chance that we got to read and understand code written by other people. This is very important skill to have when you work in the industry and can be sometimes more difficult than writing code from scratch.