Rspec limit reuse of methods

Viewed 107

New to Ruby and RSpec so please be gentle.

I'm from the Python world, pytest specifically. One of the things that you can do in pytest is use fixtures in your test and in other fixtures. In an ultra simple example, let's say we have a create fixture, and a delete fixture that requires create. If we had a test that uses both the create and delete fixtures, create would only be called once (even though delete uses create, pytest knows it's already been called for the test case. Here's some sample python code:

# test_fixture_reuse.py
import pytest

@pytest.fixture
def create_something():
    """Create tester object"""
    #return MyTester(request.param)
    print ("created_something")


@pytest.fixture
def delete_something(create_something):
    """Create tester object"""
    #return MyTester(request.param)
    print ("delete_something")


class TestIt:
    def test_only_create(self, create_something):
        print ("in test_only_create")
        assert 1

    def test_create_and_delete(self, create_something, delete_something):
        print ("in test_create_and_delete")
        assert 1

    def test_only_delete(self, delete_something):
        print ("in test_only_delete")
        assert 1

Running pytest -v test_fixture_reuse -s returns this output, as expected:

test_parameterized_fixture.py::TestIt::test_only_create created_something
in test_only_create
PASSED
test_parameterized_fixture.py::TestIt::test_create_and_delete created_something
delete_something
in test_create_and_delete
PASSED
test_parameterized_fixture.py::TestIt::test_only_delete created_something
delete_something
in test_only_delete
PASSED

From what I've tried in Ruby, there is no equivalent concept to fixture and this means that each test needs to control the flow of calls. So if we're testing delete, the test would need to call create, and then call delete, but delete would have no explicit dependency on create. Fixtures in RSpec generally seem to map to models, so most of the search results are not relevant.

My question is - how can I do something similar to the python code above in RSpec? Or is this one of those things where I'm too boxed in of my view because of Python experience, and there's an analogous way of doing things in RSpec that I'm not searching for correctly?

1 Answers

in RSpec we use let for memoized helper methods, something like this:

RSpec.describe("my tests") do
  let(:created_record) { MyRecord.create }

  # using tap here to return the deleted record from the block
  let(:destroyed_record) { created_record.tap(&:destroy) } 

  it("can create record") { created_record }
  it("can destroy record") { destroyed_record }
  it("memoizes created_record if called twice") { created_record; destroyed_record }
end

The let calls don't run until the first time you reference them in your tests, and use cached results for subsequent calls in that test case. By "test case" i mean it block. If you want these methods to execute automatically at the beginning of each test case (without having to explicitly call the method) you can use let! instead. The results will be still be cached for subsequent calls. See https://relishapp.com/rspec/rspec-core/v/2-11/docs/helper-methods/let-and-let

It's also worth reading up on subject https://relishapp.com/rspec/rspec-core/v/3-10/docs/subject as well.

If you want a normal, un-cached method, you have to just write a regular def .. end. See https://relishapp.com/rspec/rspec-core/v/3-10/docs/helper-methods/arbitrary-helper-methods

One other thing, the term "fixture" in Rails land has a somewhat specific meaning, what it usually means is that you load your database state from a YAML file or something like that (see https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures) ... this is in contrast to "factories" which are much easier to work with but also significantly slower (see https://github.com/thoughtbot/factory_bot which is the most common library for making factories).

Related