# Add Rubyzen Tests

> Write unit tests for Rubyzen's own API components — declarations, providers, collections, RSpec matchers, or Minitest assertions. Use this skill when the user wants to add tests for an existing or newly added Rubyzen component, increase test coverage, or write specs for untested methods. Also trigger when the user says "test this declaration", "add specs for", or "write tests for the X collection". Use when this capability is needed.

- Skill: `tomevault-io/add-rubyzen-tests` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/add-rubyzen-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/add-rubyzen-tests/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/add-rubyzen-tests

---


# Writing Unit Tests for Rubyzen

You are writing unit tests for Rubyzen's internal API — the declarations, providers, collections, RSpec matchers, and Minitest assertions that make up the library. These tests verify that Rubyzen's own code works correctly.

**This is NOT about writing lint rules.** Lint rules test user codebases; unit tests test Rubyzen itself. Use the `write-lint-rule` skill for lint rules.

Rubyzen has **two unit-test suites**: RSpec specs in `spec/` cover the core API (declarations, collections, providers) and the RSpec matchers; Minitest tests in `test/` cover the Minitest assertions (`assert_zen_*`).

## Step 0: Understand the Test Infrastructure

Read these files first:

1. `spec/spec_helper.rb` — test configuration
2. `spec/support/parse_helper.rb` — the `parse_ruby` helper
3. An existing spec similar to what you're testing (e.g., `spec/declarations/class_declaration_spec.rb`)

The `parse_ruby` helper is the foundation of all tests:

```ruby
def parse_ruby(source, file_path: 'test.rb')
  processed = RuboCop::AST::ProcessedSource.new(source, RUBY_VERSION.to_f, file_path)
  Rubyzen::Declarations::FileDeclaration.new(file_path, processed.ast)
end
```

It takes an inline Ruby string and returns a `FileDeclaration`, bypassing file I/O.

## Critical: The Single-Statement AST Gotcha

When a Ruby snippet has **only one statement**, the AST root node IS that statement. Providers use `node.each_descendant` which only searches children — so the root node is invisible.

```ruby
# BAD — single statement, root IS the :casgn node
file = parse_ruby('MAX = 100')
file.constants  # => empty! each_descendant can't find the root

# GOOD — two statements, root is :begin wrapper
file = parse_ruby("MAX = 100\nx = 1")
file.constants  # => [ConstantDeclaration(MAX)]
```

**Always include at least two statements** in snippets that test file-level providers (`constants`, `requires`, `blocks`, `call_sites`). This does NOT affect class/method-level tests since those always have wrapper nodes.

## Writing Declaration Specs

File: `spec/declarations/<concept>_declaration_spec.rb`

**Test every public method** on the declaration. Group related methods.

```ruby
require 'spec_helper'

RSpec.describe Rubyzen::Declarations::<Concept>Declaration do
  describe '#name' do
    it 'returns the declaration name' do
      file = parse_ruby(<<~RUBY)
        class Foo
          def bar
            # code containing the concept
          end
        end
      RUBY

      decl = file.classes.first.instance_methods.first.<concepts>.first
      expect(decl.name).to eq('expected')
    end
  end

  # Test every other public method in its own describe block...

  # Always test provider-inherited methods:
  describe '#file_path' do
    it 'returns the file path' do
      file = parse_ruby(<<~RUBY, file_path: 'app/models/user.rb')
        class User
          def foo
            # concept here
          end
        end
      RUBY

      decl = file.classes.first.instance_methods.first.<concepts>.first
      expect(decl.file_path).to eq('app/models/user.rb')
    end
  end

  describe '#line' do
    it 'returns the line number' do
      file = parse_ruby(<<~RUBY)
        class Foo
          def bar
            # concept on line 3
          end
        end
      RUBY

      decl = file.classes.first.instance_methods.first.<concepts>.first
      expect(decl.line).to eq(3)
    end
  end

  describe '#class_name' do
    it 'returns the enclosing class name' do
      file = parse_ruby(<<~RUBY)
        class Foo
          def bar
            # concept here
          end
        end
      RUBY

      decl = file.classes.first.instance_methods.first.<concepts>.first
      expect(decl.class_name).to eq('Foo')
    end
  end
end
```

## Writing Collection Specs

File: `spec/collections/<concepts>_collection_spec.rb`

Test three categories:

1. **CollectionFilterProvider methods** (`with_name`, `without_name`, etc.)
2. **Domain-specific filter methods** (e.g., `with_receiver`, `with_exception_type`)
3. **Type preservation** — `filter` returns the same collection type

```ruby
require 'spec_helper'

RSpec.describe Rubyzen::Collections::<Concepts>Collection do
  # CollectionFilterProvider
  describe '#with_name' do
    it 'filters by exact name' do
      file = parse_ruby(<<~RUBY)
        class Foo
          def bar
            # two concepts with different names
          end
        end
      RUBY

      collection = file.classes.first.instance_methods.first.<concepts>
      result = collection.with_name('target')
      expect(result.size).to eq(1)
      expect(result.first.name).to eq('target')
    end
  end

  # Domain-specific filters
  describe '#with_custom_filter' do
    it 'filters by custom criteria' do
      # ...
    end
  end

  # Type preservation
  describe '#filter' do
    it 'returns the same collection type' do
      file = parse_ruby(<<~RUBY)
        class Foo
          def bar
            # concept here
          end
        end
      RUBY

      collection = file.classes.first.instance_methods.first.<concepts>
      result = collection.filter { |d| d.name == 'something' }
      expect(result).to be_a(described_class)
    end
  end

  # Bridge methods (if collection has them)
  describe '#sub_collection' do
    it 'aggregates sub-declarations into typed collection' do
      # ...
    end
  end
end
```

## Writing Matcher Specs

File: `spec/matchers/<matcher_name>_matcher_spec.rb`

Test pass and fail cases. Use `raise_error(RSpec::Expectations::ExpectationNotMetError)` to test failure:

```ruby
require 'spec_helper'

RSpec.describe 'zen_empty matcher' do
  it 'passes when collection is empty' do
    file = parse_ruby(<<~RUBY)
      class Foo
        def bar; end
      end
    RUBY

    # Get an empty collection
    collection = file.classes.first.instance_methods.first.call_sites
    expect(collection).to zen_empty
  end

  it 'fails when collection is not empty' do
    file = parse_ruby(<<~RUBY)
      class Foo
        def bar
          puts "hello"
        end
      end
    RUBY

    collection = file.classes.first.instance_methods.first.call_sites
    expect {
      expect(collection).to zen_empty
    }.to raise_error(RSpec::Expectations::ExpectationNotMetError)
  end
end
```

**Note:** Do NOT use `fail_with` — it's not available. Use `raise_error(RSpec::Expectations::ExpectationNotMetError, /pattern/)` to test failure messages.

## Writing Minitest Assertion Tests

The Minitest assertions (`assert_zen_empty` / `assert_zen_true` / `assert_zen_false`) live in `lib/rubyzen/assertions/` and are tested with **Minitest**, not RSpec, under `test/` — the counterpart of the matcher specs above.

Files: `test/assertions/assert_<assertion_name>_test.rb`

You need to require `test/test_helper.rb` (which requires `rubyzen/minitest` and provides the same `parse_ruby` helper via the `ParseHelper` module), subclass `Minitest::Test`, and use `assert_raises(Minitest::Assertion)` to test failures:

```ruby
require_relative '../test_helper'

class AssertZenEmptyTest < Minitest::Test
  include ParseHelper

  def test_zen_empty_passes_when_collection_is_empty
    assert_zen_empty(Rubyzen::Collections::ClassesCollection.new)
  end

  def test_zen_empty_fails_when_collection_is_not_empty
    collection = parse_ruby('class Foo; end').classes
    error = assert_raises(Minitest::Assertion) { assert_zen_empty(collection) }
    assert_match(/Expected to be empty/, error.message)
  end
end
```

Run with `bundle exec rake test` (or `bundle exec ruby -Itest test/assertions/assert_zen_empty_test.rb`). The assertion tests cover the same cases as the matcher specs — pass/fail, `allowlist:`, `baseline:`, stale entries, custom `message:` — plus a missing block raising `ArgumentError` for `assert_zen_true` / `assert_zen_false`.

## When to Use Fixture Files

Use `spec/fixtures/` with real `.rb` files only when testing:
- `Project.new(paths)` — needs real file paths
- `FileCollection#with_paths` / `#without_paths` — path matching
- `ParseCache` — file-based caching

For everything else, use inline `parse_ruby` snippets.

## Test Snippet Best Practices

1. **Minimal snippets** — include only the Ruby code needed for the test
2. **Realistic names** — use realistic class/method names, not `Foo`/`bar` (when the name matters for the test)
3. **Multiple cases** — test edge cases (empty, nil, multiple items)
4. **Two+ statements** — for file-level concepts (see gotcha above)

## Checklist

- [ ] Spec file follows naming convention: `spec/<layer>/<concept>_spec.rb`
- [ ] Requires `spec_helper`
- [ ] Tests every public method on the class
- [ ] Tests provider-inherited methods (`file_path`, `line`, `class_name`)
- [ ] Tests `CollectionFilterProvider` methods for collections
- [ ] Tests type preservation for collection `filter`
- [ ] Snippets have 2+ statements when testing file-level providers
- [ ] Does NOT use `fail_with` (uses `raise_error` instead)
- [ ] Minitest assertion tests live in `test/`, require `test_helper`, and use `assert_raises(Minitest::Assertion)`
- [ ] `bundle exec rake` passes (RSpec `spec/` + Minitest `test/`)

---
> Source: [perrystreetsoftware/Rubyzen](https://github.com/perrystreetsoftware/Rubyzen) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-18 -->

