E2422. Reimplement questionnaire.rb

From Expertiza_Wiki
Jump to navigation Jump to search

Problem Description

The project includes re-implementation of the questionnaire.rb found in Expertiza to the Reimplementation-back-end . The current implementation includes various functionalities such as validation checks and methods. The goal is to implement these ensuring the code follows SOLID principles and is DRY in nature.

Important points

  1. The new model should use rails in built active record validation and eliminate the Validate_questionnaire method.
  2. The model should check for any overlapping functionalities from the controller and ensure all business logic is present in the model only.
  3. Eliminate methods and fields from this model that are exclusively utilized in test files so that only the required changes to the model are merged into the main branch.
  4. Ruby naming conventions should be followed for each method.
  5. Tests should be performed using Rspec covering all necessary functionalities.
  6. Comments should be present for every new functionality that is added and ensure to avoid all mistakes mentioned in the checklist document.
  7. Simplification of code is possible in various functions such as get_weighted_score and compute_weighted_score. For more details check the PR which has changes and ideas used for the implementation by the last team working on it.
  8. There is already an existing model in the Reimplementation back end . However this is essentially a copy paste from expertiza. This was done by the team working on the controller last spring in 2023. All changes should be made to the same file. Starting from scratch in your fork will be the quickest way to ensure no mix up from the existing implementation.

Methods Reimplemented

Validate(s)


Before refactoring

 def validate_questionnaire
   errors.add(:max_question_score, 'The maximum question score must be a positive integer.') if max_question_score < 1
   errors.add(:min_question_score, 'The minimum question score must be a positive integer.') if min_question_score < 0
   errors.add(:min_question_score, 'The minimum question score must be less than the maximum.') if min_question_score >= max_question_score
   results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id)
   errors.add(:name, 'Questionnaire names must be unique.') if results.present?
 end
end

After refactoring

 validates :name, presence: true
 validate :name_is_unique
 validates :max_question_score, numericality: { only_integer: true, greater_than: 0 }, presence: true
 validates :min_question_score, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, presence: true
 validate :min_less_than_max
 def name_is_unique
   # return if we do not have all the values to check
   return unless id && name && instructor_id
   # check for existing named questionnaire for this instructor that is not this questionnaire
   existing = Questionnaire.where('name = ? and instructor_id = ? and id <> ?', name, instructor_id, id)
   errors.add(:name, 'must be unique') if existing.present?
 end
 def min_less_than_max
   # do we have values if not then do not attempt to validate this
   return unless min_question_score && max_question_score
   errors.add(:min_question_score, 'must be less than max question score') if min_question_score >= max_question_score
 end

I changed the validate_questionnaire from a method that needs to be called for validation into individual validates in the order I expected so they could each have single responsibility.

get_weighted_score


Before refactoring

 def get_weighted_score(assignment, scores)
   # create symbol for "varying rubrics" feature -Yang
   round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round
   questionnaire_symbol = if round.nil?
                            symbol
                          else
                            (symbol.to_s + round.to_s).to_sym
                          end
   compute_weighted_score(questionnaire_symbol, assignment, scores)
 end
 def compute_weighted_score(symbol, assignment, scores)
   aq = assignment_questionnaires.find_by(assignment_id: assignment.id)
   if scores[symbol][:scores][:avg].nil?
     0
   else
     scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0
   end
 end

After refactoring

 def get_weighted_score(assignment, scores)
   compute_weighted_score(questionnaire_symbol(assignment), assignment, scores)
 end
 def compute_weighted_score(symbol, assignment, scores)
   aq = assignment_questionnaires.find_by(assignment_id: assignment.id)
   scores[symbol][:scores][:avg].nil? ? 0 : scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0
 end
 def questionnaire_symbol(assignment)
   # create symbol for "varying rubrics" feature
   # Credit ChatGPT to help me get from the inline below to the used inline, the yield self allowed me the work I wanted to do
   # AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id)&.used_in_round ? "#{symbol}#{round}".to_sym : symbol
   AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id)&.used_in_round&.yield_self { |round| "#{symbol}#{round}".to_sym } || symbol
 end

I decided to change this into two public methods and one private method. Since the original get_weighted_score got the score and looked up the symbol to use I felt it violated the single use principle. I created a private method to do the lookup of the symbol to use on its own. I created an elegant line of code to attempt to find an assignment questionnaire and if not null and used in a round it yields the result to be used in the return value. If not then it will just use this symbol. I had this line but without the yield as I was attempting to get it doing what I wanted it to do. I asked chatGPT and it presented me with the yield statement which I then used. Now each method only does what it should, gets the results, computes the result, or evaluates the symbol. 

true_false_questions?


 def true_false_questions?
   questions.each { |question| return true if question.type == 'Checkbox' }
   false
 end

This method was doing the job very succinctly already. I made no direct changes to this method and left it as is.

delete


Before refactoring

 def delete
   assignments.each do |assignment|
     raise "The assignment #{assignment.name} uses this questionnaire.
           Do you want to <A href='../assignment/delete/#{assignment.id}'>delete</A> the assignment?"
   end
   questions.each(&:delete)
   node = QuestionnaireNode.find_by(node_object_id: id)
   node.destroy if node
   destroy
 end

After refactoring

 def delete
   # Check to see if we go further? we cannot proceed if there are any assignments
   assignment = assignments.first
   if assignment
     raise "The assignment #{assignment.name} uses this questionnaire. Do you want to <A href='../assignment/delete/#{assignment.id}'>delete</A> the assignment?"
   end
   questions.each(&:delete)
   node = QuestionnaireNode.find_by(node_object_id: id)
   node&.destroy
   destroy
 end

I did not have to alter this method very much. First of all there was no reason to do an each on the list if the first one was going to throw an exception already, so I simplified that. Also just doing a null check on the dereference to the destroy on questionnaire node was more succinct than doing an if.

max_possible_score

Before refactoring

 def max_possible_score
   results = Questionnaire.joins('INNER JOIN questions ON questions.questionnaire_id = questionnaires.id')
                          .select('SUM(questions.weight) * questionnaires.max_question_score as max_score')
                          .where('questionnaires.id = ?', id)
   results[0].max_score
 end

After refactoring

 def max_possible_score
   ## Just return 0 if there are no questions; don't throw an error.
   return 0 if questions.empty?
   ## Sums up the weight of all the questions. This is not necessarily 1.
   sum_of_weights = questions.sum{ | question| quesitons.weight}
   max_possible_score = sum_of_weights * max_possible_score
 end

This refactoring is similar to E2319. Reimplement questionnaire.rb. Instead of using the complicated SQL query, this should be easier to understand and is rather straightforward. Additionally, we added comments to make it very clear what the logic was doing. Adding the error check at the beginning should make the code slightly more robust in the case of questionnaires with no questions, and the variable names have been changed for clarity.

copy_questionnaire_details


Before refactoring

 def self.copy_questionnaire_details(params)
   orig_questionnaire = Questionnaire.find(params[:id])
   questions = Question.where(questionnaire_id: params[:id])
   questionnaire = orig_questionnaire.dup
   questionnaire.name = 'Copy of ' + orig_questionnaire.name
   questionnaire.created_at = Time.zone.now
   questionnaire.updated_at = Time.zone.now
   questionnaire.save!
   questions.each do |question|
     new_question = question.dup
     new_question.questionnaire_id = questionnaire.id
     new_question.save!
   end
   questionnaire
 end

After refactoring

def self.copy_questionnaire_details(params)
   orig_questionnaire = find(params[:id])
   questionnaire = orig_questionnaire.dup
   questionnaire.name = "Copy of #{orig_questionnaire.name}"
   questionnaire.created_at = Time.zone.now
   questionnaire.updated_at = Time.zone.now
   ActiveRecord::Base.transaction do
     questionnaire.save!
     orig_questionnaire.questions.each do |question|
       new_question = question.dup
       new_question.questionnaire_id = questionnaire.id
       new_question.save!
     end
   end
   questionnaire
 end

I have removed the unnecessary assignment of questions variable since we can directly access questions associated with the original questionnaire. I used string interpolation, instead of concatenation, for setting the name of the copied questionnaire. I wrapped the copying process in a transaction to ensure data consistency. Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.

Required migrations

The following migrations were required to bring the current reimplementation fork up to par with what was needed to match the Expertiza class.

class AddUsedInRoundToAssignmentQuestionnaire < ActiveRecord::Migration[7.0]

 def self.up
   add_column 'assignment_questionnaires', 'used_in_round', :integer
 end
 def self.down
   remove_column 'assignment_questionnaires', 'used_in_round'
 end

end

class AddQuestionnarieWeightToAssignmentQuestionnaire < ActiveRecord::Migration[7.0]

 def self.up
   add_column 'assignment_questionnaires', 'questionnaire_weight', :integer, default: 0, null: false
 end
 def self.down
   remove_column 'assignment_questionnaires', 'questionnaire_weight'
 end

end

class CreateNodes < ActiveRecord::Migration[7.0]

 def self.up
   create_table :nodes do |t|
     t.column :parent_id, :integer
     t.column :node_object_id, :integer
     t.column :type, :string
   end
 end
 def self.down
   drop_table :nodes
 end

end

Rspec Testing

Suggested test skeleton


  # Here are the skeleton rspec tests to be implemented as well, or to replace existing duplicate tests
  describe "#get_weighted_score" do
    context "when the assignment has a round" do
      it "computes the weighted score using the questionnaire symbol with the round appended" do
        # Test case 1
        # Given an assignment with an ID and a questionnaire with a symbol
        # And the questionnaire is used in a round
        # And a set of scores
        # When the get_weighted_score method is called with the assignment and scores
        # Then it should compute the weighted score using the questionnaire symbol with the round appended
        # Test case 2
        # Given an assignment with an ID and a questionnaire with a symbol
        # And the questionnaire is used in a round
        # And a different set of scores
        # When the get_weighted_score method is called with the assignment and scores
        # Then it should compute the weighted score using the questionnaire symbol with the round appended
      end
    end
    context "when the assignment does not have a round" do
      it "computes the weighted score using the questionnaire symbol" do
        # Test case 3
        # Given an assignment with an ID and a questionnaire with a symbol
        # And the questionnaire is not used in a round
        # And a set of scores
        # When the get_weighted_score method is called with the assignment and scores
        # Then it should compute the weighted score using the questionnaire symbol
        # Test case 4
        # Given an assignment with an ID and a questionnaire with a symbol
        # And the questionnaire is not used in a round
        # And a different set of scores
        # When the get_weighted_score method is called with the assignment and scores
        # Then it should compute the weighted score using the questionnaire symbol
      end
    end
  end
  describe "#compute_weighted_score" do
    context "when the average score is nil" do
      it "returns 0" do
        # Test scenario
      end
    end
    context "when the average score is not nil" do
      it "calculates the weighted score based on the questionnaire weight" do
        # Test scenario
      end
    end
  end
  describe "#true_false_questions?" do
    context "when there are true/false questions" do
      it "returns true" do
        # Test scenario 1: Multiple questions with type 'Checkbox'
        # Test scenario 2: Single question with type 'Checkbox'
      end
    end
    context "when there are no true/false questions" do
      it "returns false" do
        # Test scenario 1: Multiple questions with no 'Checkbox' type
        # Test scenario 2: Single question with no 'Checkbox' type
      end
    end
  end
  describe "#delete" do
    context "when there are assignments using the questionnaire" do
      it "raises an error with a message asking if the user wants to delete the assignment" do
        # Test scenario 1
        # Given: There are assignments using the questionnaire
        # When: The delete method is called
        # Then: An error is raised with a message asking if the user wants to delete the assignment
        # Test scenario 2
        # Given: There are multiple assignments using the questionnaire
        # When: The delete method is called
        # Then: An error is raised for each assignment with a message asking if the user wants to delete the assignment
      end
    end
    context "when there are no assignments using the questionnaire" do
      it "deletes all the questions associated with the questionnaire" do
        # Test scenario 1
        # Given: There are no assignments using the questionnaire
        # When: The delete method is called
        # Then: All the questions associated with the questionnaire are deleted
        # Test scenario 2
        # Given: There are no assignments using the questionnaire and there are multiple questions
        # When: The delete method is called
        # Then: All the questions associated with the questionnaire are deleted
      end
      it "deletes the questionnaire node if it exists" do
        # Test scenario 1
        # Given: There are no assignments using the questionnaire and the questionnaire node exists
        # When: The delete method is called
        # Then: The questionnaire node is deleted
        # Test scenario 2
        # Given: There are no assignments using the questionnaire and the questionnaire node does not exist
        # When: The delete method is called
        # Then: No error is raised and the method completes successfully
      end
      it "deletes the questionnaire" do
        # Test scenario 1
        # Given: There are no assignments using the questionnaire
        # When: The delete method is called
        # Then: The questionnaire is deleted
        # Test scenario 2
        # Given: There are no assignments using the questionnaire and there are multiple questionnaires
        # When: The delete method is called
        # Then: The questionnaire is deleted
      end
    end
  end
  describe "#max_possible_score" do
    context "when the questionnaire has no questions" do
      it "returns 0 as the maximum possible score" do
        # test code
      end
    end
    context "when the questionnaire has questions with different weights" do
      it "returns the correct maximum possible score based on the weights and max_question_score" do
        # test code
      end
    end
    context "when the questionnaire has questions with the same weight" do
      it "returns the correct maximum possible score based on the weight and max_question_score" do
        # test code
      end
    end
    context "when the questionnaire ID does not exist" do
      it "returns nil as the maximum possible score" do
        # test code
      end
    end
  end
  describe '.copy_questionnaire_details' do
    context 'when given valid parameters' do
      it 'creates a copy of the questionnaire with the instructor_id' do
        # Test body
      end
      it 'sets the name of the copied questionnaire as "Copy of [original name]"' do
        # Test body
      end
      it 'sets the created_at timestamp of the copied questionnaire to the current time' do
        # Test body
      end
      it 'saves the copied questionnaire' do
        # Test body
      end
      it 'creates copies of all the questions from the original questionnaire' do
        # Test body
      end
      it 'sets the questionnaire_id of the copied questions to the id of the copied questionnaire' do
        # Test body
      end
      it 'sets the size of the copied criterion and text response questions to "50,3" if size is nil' do
        # Test body
      end
      it 'saves the copied questions' do
        # Test body
      end
      it 'creates copies of all the question advices associated with the original questions' do
        # Test body
      end
      it 'sets the question_id of the copied question advices to the id of the copied question' do
        # Test body
      end
      it 'saves the copied question advices' do
        # Test body
      end
      it 'returns the copied questionnaire' do
        # Test body
      end
    end
  end
  describe "#validate_questionnaire" do
    context "when the maximum question score is less than 1" do
      it "should add an error message" do
        # test code
      end
    end
    context "when the minimum question score is less than 0" do
      it "should add an error message" do
        # test code
      end
    end
    context "when the minimum question score is greater than or equal to the maximum question score" do
      it "should add an error message" do
        # test code
      end
    end
    context "when a questionnaire with the same name and instructor already exists" do
      it "should add an error message" do
        # test code
      end
    end
  end


Validate(s)


 describe '#name' do
   # Test validates the name of the questionnaire
   it 'returns the name of the Questionnaire' do
     # Act Assert
     expect(questionnaire.name).to eq('abc')
     expect(questionnaire1.name).to eq('xyz')
     expect(questionnaire3.name).to eq('pqr')
   end
   # Test ensures that the name field of the questionnaire is not blank
   it 'Validate presence of name which cannot be blank' do
     # Arrange
     questionnaire.name = '  '
     # Act Assert
     expect(questionnaire).not_to be_valid
   end
   # Test ensures that the name field of the questionnaire is unique per instructor
   it 'Validate name field must be unique per instructor' do
     # Arrange Act
     questionnaire.save!
     questionnaire1.name = questionnaire.name
     questionnaire3.name = questionnaire.name
     instructor2 = Instructor.create(name: 'testinstructortwo', email: 'test2@test.com', full_name: 'Test Instructor 2', password: '123456', role: role)
     instructor2.save!
     questionnaire3.instructor_id = instructor2.id
     # Assert
     expect(questionnaire).to be_valid
     expect(questionnaire1).not_to be_valid
     expect(questionnaire3).to be_valid
   end
 end
 describe '#instructor_id' do
   # Test validates the instructor id in the questionnaire 
   it 'returns the instructor id' do
     expect(questionnaire.instructor_id).to eq(instructor.id)
   end
 end
 describe '#maximum_score' do
   # Test validates the maximum score in the questionnaire 
   it 'validate maximum score' do
     expect(questionnaire.max_question_score).to eq(10)
   end
   # Test ensures maximum score is an integer
   it 'validate maximum score is integer' do
     expect(questionnaire.max_question_score).to eq(10)
     questionnaire.max_question_score = 'a'
     expect(questionnaire).not_to be_valid
     questionnaire.max_question_score = 1.1
     expect(questionnaire).not_to be_valid
   end
   # Test ensures maximum score is positive
   it 'validate maximum score should be positive' do
     expect(questionnaire.max_question_score).to eq(10)
     questionnaire.max_question_score = -10
     expect(questionnaire).not_to be_valid
     questionnaire.max_question_score = 0
     expect(questionnaire).not_to be_valid
   end
   # Test ensures maximum score is greater than the minimum score
   it 'validate maximum score should be bigger than minimum score' do
     expect(questionnaire).to be_valid
     questionnaire.max_question_score = 0
     expect(questionnaire).not_to be_valid
     questionnaire.max_question_score = 2
     questionnaire.min_question_score = 3
     expect(questionnaire).not_to be_valid
   end
 end
 describe '#minimum_score' do
   # Test validates minimum score of a questionnaire
   it 'validate minimum score' do
     questionnaire.min_question_score = 5
     expect(questionnaire.min_question_score).to eq(5)
   end
   # Test ensures minimum score is smaller than maximum score
   it 'validate minimum should be smaller than maximum' do
     expect(questionnaire.min_question_score).to eq(0)
     questionnaire.min_question_score = 10
     expect(questionnaire).not_to be_valid
     questionnaire.min_question_score = 0
   end
   # Test ensures minimum score is an integer
   it 'validate minimum score is integer' do
     expect(questionnaire.min_question_score).to eq(0)
     questionnaire.min_question_score = 'a'
     expect(questionnaire).not_to be_valid
   end
 end
 describe 'associations' do
   # Test validates the association that a questionnaire comprises of several questions
   it 'has many questions' do
     expect(questionnaire.questions).to include(question1, question2)
   end
 end

get_weighted_score


 describe '#get_weighted_score' do
   before :each do
     @questionnaire = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
     @assignment = FactoryBot.create(:assignment)
   end
   context 'when the assignment has a round' do
     it 'computes the weighted score using the questionnaire symbol with the round appended' do
       # Test case 1
       # Arrange
       FactoryBot.create(:assignment_questionnaire, { assignment_id: @assignment.id, questionnaire_id: @questionnaire.id, used_in_round: 1 })
       scores = { "#{@questionnaire.symbol}#{1}".to_sym => { scores: { avg: 100 } } }
       # Act Assert
       expect(@questionnaire.get_weighted_score(@assignment, scores)).to eq(100)
       # Test case 2
       # Arrange
       scores = { "#{@questionnaire.symbol}#{1}".to_sym => { scores: { avg: 75 } } }
       # Act Assert
       expect(@questionnaire.get_weighted_score(@assignment, scores)).to eq(75)
     end
   end
   context 'when the assignment does not have a round' do
     it 'computes the weighted score using the questionnaire symbol' do
       # Test case 3
       # Arrange
       FactoryBot.create(:assignment_questionnaire, { assignment_id: @assignment.id, questionnaire_id: @questionnaire.id })
       scores = { @questionnaire.symbol => { scores: { avg: 100 } } }
       # Act Assert
       expect(@questionnaire.get_weighted_score(@assignment, scores)).to eq(100)
       # Test case 4
       # Arrange
       scores = { @questionnaire.symbol => { scores: { avg: 75 } } }
       # Act Assert
       expect(@questionnaire.get_weighted_score(@assignment, scores)).to eq(75)
     end
   end
   describe "#compute_weighted_score" do
     before :each do
       @questionnaire = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       @assignment = FactoryBot.create(:assignment)
       FactoryBot.create(:assignment_questionnaire, { assignment_id: @assignment.id, questionnaire_id: @questionnaire.id, questionnaire_weight: 50 })
     end
     context "when the average score is nil" do
       it "returns 0" do
         # Test scenario
         # Arrange
         scores = { @questionnaire.symbol => { scores: { avg: nil } } }
         # Act Assert
         expect(@questionnaire.compute_weighted_score(@questionnaire.symbol, @assignment, scores)).to eq(0)
       end
     end
     context "when the average score is not nil" do
       it "calculates the weighted score based on the questionnaire weight" do
         # Test scenario
         # Arrange
         scores = { @questionnaire.symbol => { scores: { avg: 75 } } }
         # Act Assert
         expect(@questionnaire.compute_weighted_score(@questionnaire.symbol, @assignment, scores)).to eq(75 * 0.5)
       end
     end
   end
 end

true_false_questions?


 describe "#true_false_questions?" do
   before :each do
     @questionnaire = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
   end
   context "when there are true/false questions" do
     it "returns true" do
       # Test scenario 2: Single question with type 'Checkbox'
       # Arrange
       @questionnaire.questions.create(weight: 1, id: 1, seq: 1, txt: "que 1", question_type: "Checkbox", break_before: true)
       # Act Assert
       expect(@questionnaire.true_false_questions?).to eq(true)
       # Test scenario 1: Multiple questions with type 'Checkbox'
       # Arrange
       @questionnaire.questions.create(weight: 1, id: 2, seq: 1, txt: "que 1", question_type: "Checkbox", break_before: true)
       @questionnaire.questions.create(weight: 1, id: 3, seq: 1, txt: "que 1", question_type: "Checkbox", break_before: true)
       # Act Assert
       expect(@questionnaire.true_false_questions?).to eq(true)
     end
   end
   context "when there are no true/false questions" do
     it "returns false" do
       # Test scenario 2: Single question with no 'Checkbox' type
       # Arrange
       @questionnaire.questions.create(weight: 1, id: 1, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       # Act Assert
       expect(@questionnaire.true_false_questions?).to eq(false)
       # Test scenario 1: Multiple questions with no 'Checkbox' type
       # Arrange
       @questionnaire.questions.create(weight: 1, id: 2, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       @questionnaire.questions.create(weight: 1, id: 3, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       # Act Assert
       expect(@questionnaire.true_false_questions?).to eq(false)
     end
   end
 end

delete


 describe "#delete" do
   context "when there are assignments using the questionnaire" do
     it "raises an error with a message asking if the user wants to delete the assignment" do
       # Arrange
       questionnaire_single_assignment = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       single_assignment = FactoryBot.create(:assignment)
       FactoryBot.create(:assignment_questionnaire, { assignment_id: single_assignment.id, questionnaire_id: questionnaire_single_assignment.id})
       questionnaire_multi_assignment = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       multi_assignment1 = FactoryBot.create(:assignment)
       FactoryBot.create(:assignment_questionnaire, { assignment_id: multi_assignment1.id, questionnaire_id: questionnaire_multi_assignment.id})
       multi_assignment2 = FactoryBot.create(:assignment)
       FactoryBot.create(:assignment_questionnaire, { assignment_id: multi_assignment2.id, questionnaire_id: questionnaire_multi_assignment.id})
       # Test scenario 1
       # Given: There are assignments using the questionnaire
       # When: The delete method is called
       # Then: An error is raised with a message asking if the user wants to delete the assignment
       expect { questionnaire_single_assignment.delete }.to raise_exception(RuntimeError) do |error|
         expect(error.message).to match(/^The assignment .* uses this questionnaire/)
       end
       # Test scenario 2
       assignment1_pattern = /^The assignment #{Regexp.escape(multi_assignment1.name)} uses this questionnaire/
       assignment2_pattern = /^The assignment #{Regexp.escape(multi_assignment2.name)} uses this questionnaire/
       # Given: There are multiple assignments using the questionnaire
       # When: The delete method is called
       # Then: An error is raised for each assignment with a message asking if the user wants to delete the assignment
       expect { questionnaire_multi_assignment.delete }.to raise_exception(RuntimeError) do |error|
         expect(error.message).to match(assignment1_pattern)
       end
       multi_assignment1.destroy!
       expect { questionnaire_multi_assignment.delete }.to raise_exception(RuntimeError) do |error|
         expect(error.message).to match(assignment2_pattern)
       end
     end
   end
   context "when there are no assignments using the questionnaire" do
     it "deletes all the questions associated with the questionnaire" do
       # Test scenario 1
       # Given: There are no assignments using the questionnaire
       # When: The delete method is called
       # Then: All the questions associated with the questionnaire are deleted
       # Arrange
       questionnaire_one_question = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       question1 = questionnaire_one_question.questions.build(weight: 1, id: 1, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       question1.save!
       # Act
       questionnaire_one_question.delete
       # Assert
       expect(Question.find_by(id: question1.id)).to be_nil
       # Test scenario 2
       # Given: There are no assignments using the questionnaire and there are multiple questions
       # When: The delete method is called
       # Then: All the questions associated with the questionnaire are deleted
       # Arrange
       questionnaire_multi_question = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       question1 = questionnaire_multi_question.questions.build(weight: 1, id: 1, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       question1.save!
       question2 = questionnaire_multi_question.questions.build(weight: 1, id: 2, seq: 1, txt: "que 1", question_type: "Scale", break_before: true)
       question2.save!
       # Act
       questionnaire_multi_question.delete
       # Assert
       expect(Question.find_by(id: question1.id)).to be_nil
       expect(Question.find_by(id: question2.id)).to be_nil
     end
     it "deletes the questionnaire node if it exists" do
       # Test scenario 1
       # Given: There are no assignments using the questionnaire and the questionnaire node exists
       # When: The delete method is called
       # Then: The questionnaire node is deleted
       questionnaire = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       q_node = QuestionnaireNode.create(parent_id: 0, node_object_id: questionnaire.id, type: 'QuestionnaireNode')
       # Act
       questionnaire.delete
       # Assert
       expect(QuestionnaireNode.find_by(id: q_node.id)).to be_nil
       # Test scenario 2
       # Given: There are no assignments using the questionnaire and the questionnaire node does not exist
       # When: The delete method is called
       # Then: No error is raised and the method completes successfully
       questionnaire = FactoryBot.create(:review_questionnaire, { instructor_id: instructor.id })
       # Act Assert
       expect { questionnaire.delete }.not_to raise_error
       expect(Questionnaire.find_by(id: questionnaire.id)).to be_nil
     end

copy_questionnaire_details


describe '.copy_questionnaire_details' do
   # Test ensures calls from the method copy_questionnaire_details
   it 'allowing calls from copy_questionnaire_details' do
     allow(Questionnaire).to receive(:find).with('1').and_return(questionnaire)
     allow(Question).to receive(:where).with(questionnaire_id: '1').and_return([Question])
   end
   # Test ensures creation of a copy of given questionnaire
   # Combined two tests into one to avoid performing the same functionalities again to test another feature.
   it 'creates a copy of the questionnaire' do
     instructor.save!
     questionnaire.save!
     question1.save!
     question2.save!
     # Stub the where method to return the questions associated with the original questionnaire
     allow(questionnaire).to receive_message_chain(:questions, :each).and_yield(question1).and_yield(question2)
     # Call the copy_questionnaire_details method
     copied_questionnaire = Questionnaire.copy_questionnaire_details({ id: questionnaire.id })
     # Assertions
     expect(copied_questionnaire.instructor_id).to eq(questionnaire.instructor_id)
     expect(copied_questionnaire.name).to eq("Copy of #{questionnaire.name}")
     expect(copied_questionnaire.created_at).to be_within(1.second).of(Time.zone.now)
     # Verify that the copied questionnaire has associated questions
     expect(copied_questionnaire.questions.count).to eq(2)
     expect(copied_questionnaire.questions.first.txt).to eq(question1.txt)
     expect(copied_questionnaire.questions.second.txt).to eq(question2.txt)
   end
 end

The below tests were redundant


 it "deletes the questionnaire" do
       #Test scenario 1
       #Given: There are no assignments using the questionnaire
       #When: The delete method is called
       #Then: The questionnaire is deleted
       #Test scenario 2
       #Given: There are no assignments using the questionnaire and there are multiple questionnaires
       #When: The delete method is called
       #Then: The questionnaire is deleted
 end