1
votes

I'll preface this by saying I'm pretty new to Ruby and RSpec, and am working on adding tests to an existing codebase, so if there are red flags, feel free to let me know! I'm also not sure exactly how to search for this question, so apologies if it's a dupe.

Basically, I'm trying to write a test to flex a class that is instantiated with a block using mixins. I'm using Ruby 2.3 for what its worth.

Lets say we have a class:

class UserDataFile < DataFile
  def initialize(client, &block)
    ..do some stuff..
    super(client, &block)
  end
end

and and DataFile looks like:

class DataFile
  include FileUtils

  def initialize(client, &block)
    block.arity == 1 ? yield(self) : instance_eval(&block) if block_given?

  def reads
    @items
  end

  def write_to_file
    .. write @items to a file ..
  end
end

FileUtils looks like:

module FileUtils
  def add_data(x)
    new_data = generate_some_data(x)
    reads.concat(new_data)
    new_data
  end
end

So given the above code, someone could use this like:

MyModule::UserDataFile.new(client) do
  add_data("secret data")
  write_to_file
end

The output would be that the end user would write ruby file, and add arbitrary data, then write a file out by using the API.

However there are no tests for this functionality, and I'm pretty lost as to how to use RSpec to do this - given that the class I'm using/testing is using inherited mixins.

I can write basic functionality tests by executing the code in Rspec and verifying the output, but I'm lost when trying to build an expectation block and use any sort of double. A failed attempt looks like this:

expect(MyModule::UserDataFile).to receive(:new).with(client).and_yield
expect(MyModule::UserDataFile).to receive(:add_data).with(any_args)

or

allow(MyModule::UserDataFile).to receive(:new).with(client) {
        allow(MyModule::UserDataFile).to receive(:add_data).with(anything)
}

and if I try and execute the this after setting the expectations:

MyModule::UserDataFile.new(client) do
    add_data("testdata)
end

I get an error: NoMethodError: undefined method `add_data' for #<RSpec::blahblah

Due to my noob status, I'm guessing theres something simple I'm probably missing here - but I'm finding this very difficult to search for. Thanks!

1

1 Answers

1
votes

First, the operations provided by FileUtils should be tested separately, in a FileUtils test.

Then you should only validate that the block you provide to the UserDataFile is actually used. There are 2 cases - one just yields the instance, while the other uses instance_eval. You need to validate both.

For the first case you can use the yield RSpec matcher. It will look like:

expect { |b| MyModule::UserDataFile.new(client, &b) }.to yield_with_args(kind_of(UserDataFile)) }

The second one is more tricky. You can pass a block and add expectation for something done within a block. A classical example is to throw from the block and expect that symbol to be thrown:

my_proc = proc { throw :my_proc_was_called }

expect { MyModule::UserDataFile.new(client, &my_proc) }.to throw_symbol :my_proc_was_called

As for the reason you get NoMethodError - you are stubbing the add_data on the class, but the code is calling it on the instance. You will have to use allow_any_instance_of to stub the way you are trying to do