How can I mock super in ruby using rspec?
Asked Answered
V

4

25

I am extending an existing library by creating a child class which extends to the library class.

In the child class, I was able to test most of functionality in initialize method, but was not able to mock super call. The child class looks like something like below.

class Child < SomeLibrary
    def initialize(arg)
        validate_arg(arg)
        do_something
        super(arg)
    end

    def validate_arg(arg)
        # do the validation
    end

    def do_something
        @setup = true
    end
end

How can I write rspec test (with mocha) such that I can mock super call? Note that I am testing functionality of initialize method in the Child class. Do I have to create separate code path which does not call super when it is provided with extra argument?

Verdun answered 6/4, 2013 at 21:39 Comment(0)
D
25

You can't mock super, and you shouldn't. When you mock something, you are verifying that a particular message is received, and super is not a message -- it's a keyword.

Instead, figure out what behavior of this class will change if the super call is missing, and write an example that exercises and verifies that behavior.

Dynameter answered 7/4, 2013 at 0:44 Comment(4)
Does that mean I cannot write rspec test for initialize? Is it a bad practice to write unit test for initialize method? By the way, I am extending database connector library. If I do not write unit test, but write integration test, I just think it is weird if rspec test requires a connection to real database.Verdun
It doesn't make sense to me why we can't mock super. It seems useful to do so to properly test initialization behaviour in isolation, and surely super is a message sent to an instance of a class, no?Synchronous
No, super is not a message sent to an instance of a class. I'm aware of no ruby metaprogramming constructs that would make this possible, and even if it were, I'm still not convinced it's a good idea.Dynameter
@MyronMarston there are some cases where you need a Stub, for example in a mixin...Cancan
C
3

As @myron suggested you probably want to test the behavior happening in super.

But if you really want to do this, you could do:

expect_any_instance_of(A).to receive(:instance_method).and_call_original

Assuming

class B < A
  def instance_method
    super
  end
end

class A
  def instance_method
    #
  end
end

Disclaimer expect_any_instance_of are a mark of weak test (see):

This feature is sometimes useful when working with legacy code, though in general we discourage its use for a number of reasons:

The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether a specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)

Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.

It is the most complicated feature of rspec-mocks, and has historically received the most bug reports. (None of the core team actively use it, which doesn't help.)

Contumacious answered 11/12, 2020 at 9:47 Comment(0)
T
2

A good way to test this is to set an expectation of some action taken by the superclass - example :

class Some::Thing < Some
 def instance_method
    super
 end
end

and the super class:

class Some
  def instance_method
     another_method
  end

  def self.another_method # not private!
     'does a thing'
  end
end

now test :

 describe '#instance_method' do 
    it 'appropriately triggers the super class method' do
      sawm = Some::Thing.new
      expect(sawm).to receive(:another_method)
      sawm.instance_method
    end
 end

All This Determines Is That Super Was Called On the Superclass

This pattern's usefulness is dependent on how you structure your tests/what expectations you have of the child/derivative class' mutation by way of the super method being applied.

Also - pay close attention to class and instance methods, you will need to adjust allows and expects accordingly

YMMV

Twila answered 28/9, 2016 at 19:59 Comment(0)
Z
1

A bit late to this party, but what you can also do is forego using the super keyword and instead do

class Parent
  def m(*args)
  end
end

class Child < Parent
  alias super_m m

  def m(*args)
    super_m(*args)
  end
end

That way your super method is accessible like any other method and can e.g. be stubbed like any other method. The main downside is that you have to explicitly pass arguments to the call to the super method.

Zhang answered 15/10, 2019 at 14:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.