Thursday, April 25, 2024
HomeRuby On RailsWhich one to make use of? eql? vs equal? vs == ?...

Which one to make use of? eql? vs equal? vs == ? Mutant Pushed Improvement of Nation Worth Object


Just lately after introducing a brand new worth object to a undertaking I ran mutant to confirm my check protection fairly early. It turned out that I missed a number of locations in the case of exams, but additionally technical design of manufacturing code. On this publish, I’ll present you my growth course of for the Nation Worth Object.

When you concentrate on Worth Object it’s essential to get the distinction between eql?, equal? and == operators. These variations have been fairly essential within the class design course of.

What’s Worth Object?

So lengthy story brief, a price object is an object whose equality is predicated on its worth, not its id.

Code pattern

This can be a easy nation object. Its function is to guard the applying from utilizing nations that aren’t supported.

  class Nation
    SUPPORTED_COUNTRIES = [PL = "PL", NO = "NO"].freeze

    protected attr_reader :iso_code

    def initialize(iso_code)
      increase except SUPPORTED_COUNTRIES.embrace?(iso_code.to_s.upcase)
      @iso_code = iso_code
    finish

    def to_s
      iso_code.to_s
    finish

    def eql?(different)
      different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
    finish

    alias == eql?

    def hash
      iso_code.hash
    finish
  finish

In addition to that, as you possibly can see within the exams beneath, regardless of how we use the nation object, we wish it to all the time get the right worth of the nation’s iso code.

  class CountryTest < TestCase
    cowl Nation

    def test_returns_no
      assert_equal "NO", Nation.new("NO").to_s
      assert_equal "NO", Nation.new(:NO).to_s
      assert_equal "NO", Nation.new(Nation::NO).to_s
    finish

    def test_returns_pl
      assert_equal "PL", Nation.new("PL").to_s
      assert_equal "PL", Nation.new(:PL).to_s
      assert_equal "PL", Nation.new(Nation::PL).to_s
    finish

    def test_equality
      assert Nation.new(Nation::PL).eql? Nation.new(Nation::PL)
      assert Nation.new(Nation::NO).eql? Nation.new(Nation::NO)

      assert Nation.new(Nation::PL) == Nation.new(Nation::PL)
      assert Nation.new(Nation::NO) == Nation.new(Nation::NO)
    finish

    def test_only_supported_countries_allowed
      assert_raises { Nation.new("NL") }
      assert_raises { Nation.new("ger") }
      assert_nothing_raised { Nation.new("pl") }
    finish
  finish

That is our place to begin. Seems to be okay, doesn’t it? Earlier than ending the job of designing this class, let’s run mutant exams and confirm the outcomes.

We’ll deal with rising the mutant protection of equality-related strategies.

Let’s have a look at results of first bundle exec mutant run

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation) || iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different && iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  true && iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  Nation && iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  self.instance_of?(Nation) && iso_code.eql?(different.iso_code)
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation) && iso_code
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation) && true
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation) && different.iso_code
 finish

 def eql?(different)
-  different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
+  different.instance_of?(Nation) && iso_code.eql?(self.iso_code)
 finish

 def hash
-  iso_code.hash
+  increase
 finish

 def hash
-  iso_code.hash
+  tremendous
 finish

 def hash
-  iso_code.hash
 finish

 def hash
-  iso_code.hash
+  nil
 finish

 def hash
-  iso_code.hash
+  iso_code
 finish

 def hash
-  iso_code.hash
+  self.hash
 finish

The - signal symbolizes eliminated line of code. The + signal symbolizes line of code launched by mutant. So regardless that there are exams that look fairly good, the result’s poor. This causes false sense of safety.

That is the summarized rating that we’ll begin with:

Integration:     minitest
Jobs:            1
Contains:        ["test"]
Requires:        ["./config/environment", "./test/support/mutant"]
Topics:        4
Whole-Checks:     523
Chosen-Checks:  4
Checks/Topic:   1.00 avg
Mutations:       72
Outcomes:         72
Kills:           55
Alive:           17
Timeouts:        0
Runtime:         26.23s
Killtime:        23.41s
Overhead:        12.09%
Mutations/s:     2.74
Protection:        76.39%

Let’s enhance that protection!

This can be a good cut-off date to repeat the code and attempt to enhance it’s mutant protection 😉

Heal the code

At a primary look it appears like our check suite isn’t full. Let’s attempt to enhance mutant protection by including lacking exams.

    def test_values_equality
      refute Nation.new(Nation::PL).eql? Nation.new(Nation::NO)
      refute Nation.new("PL").eql? "PL"
    finish

So on this check we anticipate that

  • Nation objects of two completely different nations usually are not equal
  • Worth object isn’t the identical factor as easy string

All proper so this check removes a lot of the issues. Truly, there may be 6 extra points left:

 def hash
-  iso_code.hash
+  increase
 finish

 def hash
-  iso_code.hash
+  tremendous
 finish

 def hash
-  iso_code.hash
 finish

 def hash
-  iso_code.hash
+  nil
 finish

 def hash
-  iso_code.hash
+  iso_code
 finish

 def hash
-  iso_code.hash
+  self.hash
 finish

How can we kill these mutants?

Making hash methodology extra sturdy

  def hash
    Nation.hash ^ iso_code.hash
  finish

And run mutant once more

 def hash
-  Nation.hash ^ iso_code.hash
+  increase
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  tremendous
 finish

 def hash
-  Nation.hash ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  nil
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  nil ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  self.hash ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation.hash ^ nil
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation.hash ^ iso_code
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation.hash ^ self.hash
 finish

Properly… not good, not dangerous. Totally different mutants have been injected within the code. Nonetheless, there are some survivors.

Step again. What are we making an attempt to realize?

We’re making an attempt to design Worth Object.

Two Worth Objects are equal when:

  • they’ve the identical hash values, now we have such a check
  • when their courses are the identical

As soon as once more it appears like we’re lacking some exams.

Let’s write a check that can verify if the hash worth of two worth objects are equal.

  def test_hash_equality
    assert Nation.new(Nation::PL).hash.eql? Nation.new(Nation::PL).hash
  finish

And now let’s run mutant and see the outcomes.

 def hash
-  Nation.hash ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  nil
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  Nation.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  nil ^ iso_code.hash
 finish

 def hash
-  Nation.hash ^ iso_code.hash
+  iso_code.hash
 finish

So what’s mutant making an attempt to inform us?

In the event you’re following alongside, modify the hash methodology to one of many recommendations and see what occurs. Yep. The exams are nonetheless passing! And so they shouldn’t be, proper?

I believe we’re lacking a check to ensure the modification that we simply did can be detected if the hash methodology was modified. Particularly I imply the step that we simply did, so including the category of the Worth Object to the equation.

Let’s repair this by extending the hash_equality check case by few damaging situations testing the hash methodology.

  def test_hash_equality
    assert Nation.new(Nation::PL).hash.eql? Nation.new(Nation::PL).hash

    # new instances beneath
    assert_not_equal Nation.new(:PL).hash, (Nation.hash ^ "NO".hash)
    assert_not_equal Nation.new(:PL).hash, (Nation.hash ^ "PL".hash)
    assert_not_equal Nation.new(:PL).hash, (Nation.new(:NO).hash)
    refute Nation.new(Nation::PL).hash == "PL".hash
  finish

Now after working the mutant we’re good 🙂

Integration:     minitest
Jobs:            1
Contains:        ["test"]
Requires:        ["./config/environment", "./test/support/mutant"]
Topics:        4
Whole-Checks:     525
Chosen-Checks:  6
Checks/Topic:   1.50 avg
Mutations:       78
Outcomes:         78
Kills:           78
Alive:           0
Timeouts:        0
Runtime:         37.15s
Killtime:        33.76s
Overhead:        10.03%
Mutations/s:     2.10
Protection:        100.00%

The == operator compares two objects based mostly on their worth. For instance

1 == 1 # true
1 == 1.0 # true
1.hash == 1.0.hash #false

For easy class:

    class Klass
      attr_accessor :code

      def initialize(code)
        @code = code
      finish
    finish

The check fails

    def test_klass
      assert Klass.new("a") == Klass.new("a")
    finish

The eql? methodology compares two objects based mostly on their hash.

2.eql? 2 # true
2.eql? 2.0 # false

Couldn’t we simply do it like this…?

    def test_klass
      assert Klass.new("a").eql? Klass.new("a")
    finish

Nope.

Two objects with the identical worth. However! The hash is completely different. When the hash methodology isn’t overwritten, it’s based mostly on the article’s id. So it’s one thing that we don’t need after we take into consideration Worth Objects.

Basically, it’s higher to make use of .eql? methodology for the Worth Object if you wish to ensure that there’s no hash colision.

Why isn’t the equal? methodology additionally aliased to the eql? operator? The reason being the truth that the equal? methodology checks the id of the article.
Let’s have a look at an instance.

    def test_equality
      first = "a"
      second = "a"

      assert first.equal? second 
    finish

The check fails. Test the id of these two objects, they’re completely different.
first.__id__ != second.__id__

In addition to that, overwriting equal? isn’t beneficial.

class Nation
  SUPPORTED_COUNTRIES = [PL = "PL", NO = "NO"].freeze

  protected attr_reader :iso_code

  def initialize(iso_code)
    increase except SUPPORTED_COUNTRIES.embrace?(iso_code.to_s.upcase)
    @iso_code = iso_code
  finish

  def to_s
    iso_code.to_s
  finish

  def eql?(different)
    different.instance_of?(Nation) && iso_code.eql?(different.iso_code)
  finish

  alias == eql?

  def hash
    Nation.hash ^ iso_code.hash
  finish
finish



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments