Spec::Item.tags: could we support recursive / parent tag lookups?

Is there a cleaner way than then monkeypatch I’ve done below to automatically look up all tags applied to a Spec::Item, including not just the tags applied to the it but also the tags applied to its parent describe blocks?

Use case: some (but not all) of my specs hit a test database. As I do in Ruby / rspec, I want to clear the database before and after each spec example runs, so that each example starts with a clean, predictable state. I have a truncate_db tag applied to the relevant specs:

describe MyClassThatRequiresDatabase, tags: ["truncate_db"] do
  it "..." do
    # this test expects a clean database state, and may mutate database,
    # so we should clean the db before and after!
  end
end

In my use case, I’ve written a spec/truncate_db_helper.cr (required by my spec/spec_helper.cr) which basically looks like this:

Spec.around_each do |example_procsy|
  if !example_procsy.example.recursive_tags.includes?("truncate_db")
    example_procsy.run
    next
  end

  MyDbPool.truncate_entire_database_for_testing!
  example_procsy.run
  MyDbPool.truncate_entire_database_for_testing!
end

I was surprised when my first attempt at example_procsy.example.tags.includes?("truncate_db") only covered the case where I applied my tag directly to the it level, but it did not cover when I’d applied the tag to an entire describe group.

In order to get the desired behavior, I had to monkeypatch a spec/recursive_tags_helper.cr to add a #recursive_tags method as follows:

require "spec"

module Spec
  module Item
    protected def recursive_tags_helper(acc : Set(String)) : Nil
      acc.concat(self.tags.not_nil!) unless self.tags.nil?

      return if self.parent.is_a?(Spec::RootContext)
      self.parent.as(Spec::ExampleGroup).recursive_tags_helper(acc)
    end

    def recursive_tags : Set(String)
      # collect all tags, recursively looking up the tree of parent "describe" contexts
      results = Set(String).new
      self.as(Spec::Example | Spec::ExampleGroup).recursive_tags_helper(results)
      results
    end
  end
end

This allows me to apply the truncate_db tag at either the individual it level, or at a higher describe level.

Rspec seems to do something similar with its metadata collection, and this functionality is used commonly on the Rails side (type: :model vs type: :request for example).

Does this belong in stdlib? (Happy to make it into a PR.) Any suggestions for a better name than #recursive_tags?

1 Like

Yeah, I think it makes sense to have a way for querying all tags, including inherited ones.

Perhaps this should even be the default behaviour for #tags with a different method for accessing only the tags attached to this specific example.

But it’s probably better to go with #tags as is and #all_tags for including inherited ones.

3 Likes

OK cool, thanks! I will put together a PR for #all_tags when I get some free time.

Another option would be to use a helper method versus relying on the tags:

require "spec"

def with_clean_db(description : String, &block)
  it description do
    MyDbPool.truncate_entire_database_for_testing!
    block.call
    MyDbPool.truncate_entire_database_for_testing!
  end
end

describe "Test" do
  with_clean_db "finds newly inserted records" do
    # Queries, tests, and such
  end
end
2 Likes

@Blacksmoke16 I like that elegant solution as well!

PR here: Add `Spec::Item#all_tags` method, which includes context-inherited tags by compumike · Pull Request #12915 · crystal-lang/crystal · GitHub