Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
### Unreleased

- Add Test::Unit support, including lifecycle integration for dynamic models and
tables.
- Add `with_model/rspec`, `with_model/minitest`, and `with_model/test_unit`
convenience entrypoints.
- Add runner integration guides for RSpec, Minitest, and Test::Unit.
- Skip temporary-table teardown when the table has already disappeared.
- Require Active Record 8.0 or later.

### 2.3.1
Expand Down
23 changes: 20 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,36 @@ avoid anything that only works on the newest of either.

## Running the tests

The suite runs against both supported test harnesses, so `with_model` has to work
under each:
The suite runs against all three supported test harnesses, so `with_model` has
to work under each:

- `spec/` covers behavior under RSpec.
- `test/` covers the minitest life cycle — the setup and teardown hooks, and
- `test/` covers the Minitest life cycle — the setup and teardown hooks, and
their ordering — rather than repeating the specs.
- `test_unit/` covers Test::Unit integration in a dedicated process, including
native fixture callback ordering.

Run one harness with its scoped Rake task:

```sh
bundle exec rake spec
bundle exec rake test
bundle exec rake test_unit
```

Run everything, including the linter, with:

```sh
bundle exec rake
```

Validate the README and runner guides with RDoc before submitting documentation
changes:

```sh
rm -rf tmp/rdoc && bundle exec rdoc --markup markdown --op tmp/rdoc README.md docs/rspec.md docs/minitest.md docs/test-unit.md lib/**/*.rb
```

`bin/` holds generated binstubs and is not checked in. Whether bundling writes
them for you depends on your Bundler version, so create them once if you would
rather type `bin/rake`:
Expand Down
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,63 @@

Install as usual: `gem install with_model` or add `gem 'with_model'` to your Gemfile. See [`.github/workflows/ci.yml`](./.github/workflows/ci.yml) for supported (tested) Ruby versions.

## Test runner setup

Choose the entrypoint for the test runner that owns the declarations. Each
performs complete global setup for one runner; see its guide for lifecycle and
manual-setup details.

### RSpec

Extend `WithModel` into RSpec:
```ruby
require "with_model/rspec"
```

See the [RSpec guide](docs/rspec.md).

### Minitest

```ruby
require "with_model"
require "with_model/minitest"
```

RSpec.configure do |config|
config.extend WithModel
end
See the [Minitest guide](docs/minitest.md).

### Test::Unit

```ruby
require "with_model/test_unit"
```

### minitest/spec
See the [Test::Unit guide](docs/test-unit.md).

Extend `WithModel` into minitest/spec and set the test runner explicitly:
### Advanced and mixed-runner setup

Plain `require "with_model"` remains framework-neutral. For manual setup,
select a runner and extend its test context directly:

```ruby
require "minitest"
require "with_model"

WithModel.runner = :minitest
Minitest::Test.extend WithModel
```

class Minitest::Spec
extend WithModel
RSpec uses `RSpec.configure { |config| config.extend WithModel }`; Test::Unit
uses `Test::Unit::TestCase.extend WithModel`. A declaration can override the
global runner when needed:

```ruby
with_model :Post, runner: :test_unit do
table
end
```

Loading multiple convenience entrypoints in one process is unsupported because
the last one changes the global runner. For mixed runners, use plain
`with_model`, direct extension, and explicit `runner:` options.

## Usage

After setting up as above, call `with_model` and inside its block pass it a `table` block and a `model` block.
Expand Down Expand Up @@ -252,7 +283,13 @@ the order they are declared, and destroyed in the reverse order.

## Requirements

See the [gemspec metadata](https://rubygems.org/gems/with_model) for dependency requirements. RSpec and minitest are indirect dependencies, and `with_model` should support any maintained version of both.
See the [gemspec metadata](https://rubygems.org/gems/with_model) for Ruby and
Active Record requirements. RSpec, Minitest, and Test::Unit are optional
consumer framework dependencies; install the framework used by its selected
entrypoint. Requiring a selected entrypoint without its framework installed
raises the framework's ordinary `LoadError`.

`scope:` is RSpec-only and is silently ignored by Minitest and Test::Unit.

## Thread-safety

Expand Down
45 changes: 45 additions & 0 deletions docs/minitest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Minitest integration

Load the Minitest entrypoint before defining tests:

```ruby
require "with_model/minitest"
```

This requires `minitest`, then `with_model`, selects the
`:minitest` runner, and extends `Minitest::Test`. Its class-level DSL is
inherited by `Minitest::Test` subclasses, including `Minitest::Spec` and
`ActiveSupport::TestCase`. The entrypoint does not require or start Minitest's
autorun — your process or test framework owns Minitest startup.

## Lifecycle

The Minitest runner creates declared models and tables in `before_setup` and
destroys them in `after_teardown`. This makes them available to ordinary setup
methods and keeps them available through teardown. `scope:` is silently ignored
by Minitest because it is an RSpec-only option.

## Manual setup

If another process or tool owns Minitest configuration, configure manually:

```ruby
require "minitest"
require "with_model"

WithModel.runner = :minitest
Minitest::Test.extend WithModel
```

Minitest is an optional consumer dependency. Requiring `with_model/minitest`
without Minitest installed raises Minitest's ordinary `LoadError`.

## Mixed runners

Do not load multiple convenience entrypoints in one process: each changes the
global runner, and the last one wins. For mixed runners, load plain
`with_model`, extend each framework boundary directly, and select a runner per
declaration with `runner:`.

See the [general DSL reference](../README.md#usage) for declaring models and
tables.
43 changes: 43 additions & 0 deletions docs/rspec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# RSpec integration

Load the RSpec entrypoint before defining example groups:

```ruby
require "with_model/rspec"
```

The entrypoint requires `rspec/core` and `with_model`, selects the `:rspec`
runner, and extends RSpec example groups through `RSpec.configure`. It does not
start RSpec; the RSpec command-line runner remains in control.

## Lifecycle

`with_model` and `with_table` register RSpec `before` and `after` hooks. Models
and tables are available to examples after setup and are removed after cleanup.
Use `scope:` when RSpec hook scope is needed, including `scope: :all` for a
model needed by `before(:all)`. `scope:` is an RSpec-only option.

## Manual setup

For runner-owned setup, require the framework and `with_model` separately:

```ruby
require "rspec/core"
require "with_model"

WithModel.runner = :rspec
RSpec.configure { |config| config.extend WithModel }
```

RSpec is an optional consumer dependency. Requiring `with_model/rspec` without
RSpec installed raises RSpec's ordinary `LoadError`.

## Mixed runners

Do not load multiple convenience entrypoints in one process: each changes the
global runner, and the last one wins. For mixed runners, load plain
`with_model`, extend each framework boundary directly, and select a runner per
declaration with `runner:`.

See the [general DSL reference](../README.md#usage) for declaring models and
tables.
73 changes: 73 additions & 0 deletions docs/test-unit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Test::Unit integration

Load the Test::Unit entrypoint before defining tests:

```ruby
require "with_model/test_unit"
```

The entrypoint requires `test/unit`, then `with_model`, selects the
`:test_unit` runner, and extends `Test::Unit::TestCase`. Test::Unit retains
ownership of autorun and fixture execution.

## Lifecycle

`with_model` and `with_table` register Test::Unit fixture callbacks with exact
placement: setup uses `before: :append`, while teardown uses `after: :prepend`.
Declared models and tables therefore create in declaration order before user or
inherited setup methods and callbacks. They destroy in reverse order after user
or inherited teardown methods and callbacks.

Test::Unit runs teardown when setup raises. `WithModel::Model#destroy`
defensively handles creation that never assigned a model. `Table#destroy` checks
whether its table exists before dropping it, so teardown does not raise solely
because a reconnect replaced an in-memory database or the table otherwise
already disappeared. It does not rescue other database, connection, or drop
errors; those still propagate. This conditional drop check does not promise
generic partial-creation recovery for bare `with_table`. `scope:` is silently
ignored by Test::Unit because it is an RSpec-only option.

## Errata and concerns

Establish the Active Record connection before `with_model`'s setup callback
creates tables and models. An ordinary `def setup` runs too late. Connect in
your test helper before test classes load, or register a connection callback
with `setup(before: :prepend)`.

```ruby
class PostTest < Test::Unit::TestCase
setup(before: :prepend) do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
end

with_model :Post do
table
end
end
```

## Manual setup

For runner-owned setup, require Test::Unit and `with_model` separately:

```ruby
require "test/unit"
require "with_model"

WithModel.runner = :test_unit
Test::Unit::TestCase.extend WithModel
```

Test::Unit is an optional consumer dependency. Requiring
`with_model/test_unit` without Test::Unit installed raises Test::Unit's ordinary
`LoadError`.

## Mixed runners

Do not load multiple convenience entrypoints in one process: each changes the
global runner, and the last one wins. For mixed runners, load plain
`with_model`, extend each framework boundary directly, and select a runner per
declaration with `runner:`.

See the [general DSL reference](../README.md#usage) for declaring models and
tables.
3 changes: 2 additions & 1 deletion with_model.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Gem::Specification.new do |spec|
spec.authors = ["Case Commons, LLC", "Grant Hutchins", "Andrew Marshall"]
spec.email = %w[casecommons-dev@googlegroups.com gems@nertzy.com andrew@johnandrewmarshall.com]
spec.homepage = "https://github.com/Casecommons/with_model"
spec.summary = "Dynamically build a model within an RSpec context"
spec.summary = "Dynamically build an Active Record model for " \
"RSpec, Minitest, and Test::Unit tests"
spec.description = spec.summary
spec.license = "MIT"
spec.metadata["rubygems_mfa_required"] = "true"
Expand Down