KKP BLOG

Personal space

View on GitHub
23 March 2023

Mutations - Part I

by kkp

Mutant cases:

Case 1:

Test

RSpec.describe OffersAndOrders::TransitStatus do
  extend Support::WithEveryCombination
  subject { described_class.call(params: params, order: order) }

  let(:params) { {status: new_status, comment: comment} }
  let(:company) { create :company }
  let(:user) { create :user, role: :seller }
  let(:offer) { create :offer, company: company, user: user }
  let(:order) { create :order, offer: offer, user: user, company: company, status: Order.statuses.keys.sample, comment: "upsi" }
  let_it_be(:comment) { "hehehe" }

  context "when status is valid" do
    with_every_combination(status: Order.statuses.keys) do
      let(:new_status) { status }
      it "succeeds" do
        expect(subject).to be_success
        expect(subject.result[:status]).to eq status
        expect(order.reload.comment).to eq comment
      end
    end
  end

  context "when status is invalid" do
    let(:new_status) { "JAJCO" }
    it "fails" do
      expect(subject).to be_failure
      expect(subject.errors[0]).to eq "status Podana wartość nie jest dozwoloną wartością"
      expect(order.reload.status).to eq order.status
      expect(order.reload.comment).to eq "upsi"
    end
  end

  context "with correction" do
    let(:new_status) { "correction" }
    it "it sends an email" do
      expect { subject }.to have_enqueued_mail(SellerMailer, :send_correction_info).with(order)
    end
  end
end

Object

module OffersAndOrders
  class TransitStatus
    include BaseService

    def initialize(params:, order:)
      @params = params
      @status = params.fetch(:status)
      @comment = params.dig(:comment)
      @order = order
    end

    def call
      validation = OrderContract.new.call(**params)
      if validation.success? && order.update(status: status, comment: comment)
        send_seller_email if order.correction?
        add_result(status: status)
      else
        full_default_error_messages(validation)
      end
    end

    private

    attr_reader :comment, :order, :status, :params

    def send_seller_email
      SellerMailer.send_correction_info(order).deliver_later
    end
  end
end

Mutation Result

Mutations:       113
Results:         113
Kills:           111
Alive:           2
Timeouts:        0
Runtime:         101.93s
Killtime:        392.37s
Overhead:        -74.02%
Mutations/s:     1.11
Coverage:        98.23%

Mutation:

1.

  def call
    validation = OrderContract.new.call(**params)
    if validation.success? && order.update(status: status, comment: comment)
    -  if order.correction?
    +  if true
      send_seller_email
    end
      add_result(status: status)
    else
      full_default_error_messages(validation)
    end
  end
Changes:
  context "with email" do
    context "with correction" do
      let(:new_status) { "correction" }
      it "it sends an email" do
        expect { subject }.to have_enqueued_mail(SellerMailer, :send_correction_info).with(order)
      end
    end

    context "with non-correction" do
      with_every_combination(status: Order.statuses.except(:correction).keys) do
        let(:new_status) { status }
        it "it does NOT sends an email" do
          expect { subject }.to_not have_enqueued_mail(SellerMailer, :send_correction_info)
        end
      end
    end
  end
Conclusions:

In general mutant tips were mostly about inprecise code, which checked that the email will be sent out when the status is set to “correction”. Changing the if statement in mutant as lead to inproper behaviour where email is always sent out, but the code did not notice that, because it will still send out at correction status. Very sensible case that helps make the test more precise.

Case 2:

Test

require "rails_helper"

RSpec.describe Companies::AssignUser do
  subject(:result) { described_class.call(user: user, target: company) }
  let(:user) { create :user, role: "supervisor" }
  let!(:company) { create :company, nip: nip }
  let(:nip) { "1234567899" }

  context "assign user to company" do
    it "check relation user to company" do
      subject
      company = Company.find_by(nip: nip)
      expect(company.users).to be_present
    end
  end

  context "assign user to company branch" do
    let!(:existing_company) { create :company, nip: nip }
    let(:company) { create :company_branch, company: existing_company }

    it "check relation user to company branch" do
      subject
      company_branch = Company.find_by(nip: nip).company_branches.first
      expect(company_branch.users).to be_present
    end
  end

  it "has result data" do
    expect(result.result[:target]).to eq Company.find_by(nip: nip)
  end
end

Object

module Companies
  class AssignUser
    include BaseService

    def initialize(user:, target:)
      @target = target
      @user = user
    end

    def call
      target.users << user
      target.save!
      add_result(target: target)
    end

    private

    attr_accessor :target, :user
  end
end

Mutation result

Mutations:       38
Results:         38
Kills:           36
Alive:           2
Timeouts:        0
Runtime:         24.28s
Killtime:        90.78s
Overhead:        -73.25%
Mutations/s:     1.57
Coverage:        94.74%

Mutations

1:

 def call
   target.users << user
-  target.save!
+  target
   add_result(target: target)
 end

2:

 def call
   target.users << user
-  target.save!
+  nil
   add_result(target: target)
 end
Changes:

The object did not do enough, so this time the changes were not just in specs.

user.update(company: target.company) if target.instance_of?(CompanyBranch)

Removed this line

target.save!

Since this performs a save

target.users << user

End test:

require "rails_helper"

RSpec.describe Companies::AssignUser do
  subject(:result) { described_class.call(user: user, target: target) }
  let(:user) { create :user, role: "supervisor" }
  let!(:company) { create :company, nip: nip }
  let(:nip) { "1234567899" }
  let(:target) { company }

  context "assign user to company" do
    it "check relation user to company" do
      subject
      company = Company.find_by(nip: nip)
      expect(company.users).to be_present
      expect(user.company).to eq company
    end
  end

  context "assign user to company branch" do
    let(:company_branch) { create :company_branch, company: company }
    let(:target) { company_branch }

    it "check relation user to company branch" do
      subject
      company_branch = Company.find_by(nip: nip).company_branches.first
      expect(company_branch.users).to be_present
      expect(user.company).to eq company
      expect(user.company_branch).to eq company_branch
    end
  end

  it "has result data" do
    expect(result.result[:target]).to eq Company.find_by(nip: nip)
  end
end

Conclusion:

Both mutation pointed the same thing out: if we did not save! the object that got passed to target, the test result would be the same. Thies meant that the spec did not correctly test if the user has been assigned to the company. This has turned out to be true, taking a closer look to the spec, you can see that the Company object is checked for users number, but it was not very precise. It had assumed that the seeds/factory for the company, does not assign it a default user (and in fact it did not). The spec was based on this assumption too heavily. If the set up for the company would be change, in factory or not, the spec would become even more inprecise. In general a good example, once again, of making specs more precise, making it less vulnerable to outside changes, made to factories, databases or set ups of tests. It also helped to see a bug in actual code.

tags: