diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2b354a7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Test +on: [push, pull_request] +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + ruby: ['3.0', '3.1', '3.2'] + name: test (Ruby ${{ matrix.ruby }} on ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Set up Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20abc76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# macOS +.DS_Store + +# direnv +.direnv +.envrc + +# Editors +.nova +.vscode +*.sublime* + +# Ruby +gems.locked +pkg/* +*.gem +.bundle +.yardoc diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..a3ec5a4 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6b011b3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +## Main + +#### Initial Implementation +* Require Ruby 3.0 +* Substitution of instance variables +* Substitution of global variables +* Helper for Minitest spec notation diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5d62dd8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Sven Schwyn + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4a4b8e --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +[![Version](https://img.shields.io/gem/v/minitest-substitute.svg?style=flat)](https://rubygems.org/gems/minitest-substitute) +[![Tests](https://img.shields.io/github/actions/workflow/status/svoop/minitest-substitute/test.yml?style=flat&label=tests)](https://github.com/svoop/minitest-substitute/actions?workflow=Test) +[![Code Climate](https://img.shields.io/codeclimate/maintainability/svoop/minitest-substitute.svg?style=flat)](https://codeclimate.com/github/svoop/minitest-substitute/) +[![Donorbox](https://img.shields.io/badge/donate-on_donorbox-yellow.svg)](https://donorbox.org/bitcetera) + +# Minitest::Substitute + +Simple Minitest helper to replace values such as an instance variable of an object or an environment variable for the duration of a block or a group of tests. + +This comes in very handy when you have to derive from default configuration in order to test some aspects of your code. + +* [Homepage](https://github.com/svoop/minitest-substitute) +* [API](https://www.rubydoc.info/gems/minitest-substitute) +* Author: [Sven Schwyn - Bitcetera](https://bitcetera.com) + +## Install + +This gem is [cryptographically signed](https://guides.rubygems.org/security/#using-gems) in order to assure it hasn't been tampered with. Unless already done, please add the author's public key as a trusted certificate now: + +``` +gem cert --add <(curl -Ls https://raw.github.com/svoop/minitest-substitute/main/certs/svoop.pem) +``` + +Add the following to the Gemfile or gems.rb of your [Bundler](https://bundler.io) powered Ruby project: + +```ruby +gem 'minitest-substitute' +``` + +And then install the bundle: + +``` +bundle install --trust-policy MediumSecurity +``` + +Finally, require this gem in your `test_helper.rb` or `spec_helper.rb`: + +```ruby +require 'minitest/substitute' +``` + +## Usage + +This lightweight gem implements features on a "as needed" basis, as of now: + +* substitution of instance variables +* substitution of global variables + +Please [create an issue describing your use case](https://github.com/svoop/minitest-substitute/issues) in case you need more features such as the substitution of class variables or substitution via accessor methods. + +### Block + +To substitute the value of an instance variable for the duration of a block: + +```ruby +class Config + def initialize + @version = 1 + end +end + +Config.instance_variable_get('@version') # => 1 +with '@version', 2, on: Config do + Config.instance_variable_get('@version') # => 2 +end +Config.instance_variable_get('@version') # => 1 +``` + +Same goes for global variables: + +```ruby +$verbose = false # => false +with '$verbose', true do + $verbose # => true +end +$verbose # => false +``` + +And it works for hashes as well which comes in handy when you have to temporarily override the value of an environment variable: + +```ruby +ENV['EDITOR'] # => 'vi' +with "ENV['EDITOR']", 'nano' do + ENV['EDITOR'] # => 'nano' +end +ENV['EDITOR'] # => 'vi' +``` + +### Group of Tests + +When using spec notation, you can change a value for all tests within a `describe` group: + +```ruby +class Config + def initialize + @version = 1 + end +end + +describe Config do + subject do + Config.new + end + + describe 'original version' do + it "returns the original version" do + _(subject.instance_variable_get('@version')).must_equal 1 + end + end + + describe 'sustituted version' do + with '@version', 2, on: Config + + it "returns the substituted version" do + _(subject.instance_variable_get('@version')).must_equal 2 + end + end +end +``` + +For consistency with other Minitest helpers, the following alternative does exactly the same: + +```ruby +with '@version', on: Config do + 2 +end +``` + +(The spec integration is borrowed from [minitest-around](https://rubygems.org/gems/minitest-around) for elegance and compatibility.) + +## Development + +To install the development dependencies and then run the test suite: + +``` +bundle install +bundle exec rake # run tests once +bundle exec guard # run tests whenever files are modified +``` + +You're welcome to [submit issues](https://github.com/svoop/minitest-substitute/issues) and contribute code by [forking the project and submitting pull requests](https://docs.github.com/en/get-started/quickstart/fork-a-repo). + +## License + +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..2ecbcfc --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "minitest/substitute" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/certs/svoop.pem b/certs/svoop.pem new file mode 100644 index 0000000..b77d81b --- /dev/null +++ b/certs/svoop.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhydWJ5 +L0RDPWJpdGNldGVyYS9EQz1jb20wHhcNMjIxMTA2MTIzNjUwWhcNMjMxMTA2MTIz +NjUwWjAjMSEwHwYDVQQDDBhydWJ5L0RDPWJpdGNldGVyYS9EQz1jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcLg+IHjXYaUlTSU7R235lQKD8ZhEe +KMhoGlSUonZ/zo1OT3KXcqTCP1iMX743xYs6upEGALCWWwq+nxvlDdnWRjF3AAv7 +ikC+Z2BEowjyeCCT/0gvn4ohKcR0JOzzRaIlFUVInlGSAHx2QHZ2N8ntf54lu7nd +L8CiDK8rClsY4JBNGOgH9UC81f+m61UUQuTLxyM2CXfAYkj/sGNTvFRJcNX+nfdC +hM9r2kH1+7wsa8yG7wJ2IkrzNACD8v84oE6qVusN8OLEMUI/NaEPVPbw2LUM149H +PVa0i729A4IhroNnFNmw4wOC93ARNbM1+LW36PLMmKjKudf5Exg8VmDVAgMBAAGj +dzB1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSfK8MtR62mQ6oN +yoX/VKJzFjLSVDAdBgNVHREEFjAUgRJydWJ5QGJpdGNldGVyYS5jb20wHQYDVR0S +BBYwFIEScnVieUBiaXRjZXRlcmEuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAYG2na +ye8OE2DANQIFM/xDos/E4DaPWCJjX5xvFKNKHMCeQYPeZvLICCwyw2paE7Otwk6p +uvbg2Ks5ykXsbk5i6vxDoeeOLvmxCqI6m+tHb8v7VZtmwRJm8so0eSX0WvTaKnIf +CAn1bVUggczVdNoBXw9WAILKyw9bvh3Ft740XZrR74sd+m2pGwjCaM8hzLvrVbGP +DyYhlBeRWyQKQ0WDIsiTSRhzK8HwSTUWjvPwx7SEdIU/HZgyrk0ETObKPakVu6bH +kAyiRqgxF4dJviwtqI7mZIomWL63+kXLgjOjMe1SHxfIPo/0ji6+r1p4KYa7o41v +fwIwU1MKlFBdsjkd +-----END CERTIFICATE----- diff --git a/checksums/minitest-flash-0.1.0.gem.sha512 b/checksums/minitest-flash-0.1.0.gem.sha512 new file mode 100644 index 0000000..2b30a77 --- /dev/null +++ b/checksums/minitest-flash-0.1.0.gem.sha512 @@ -0,0 +1 @@ +c240bed743a19830b5eeeb6682ff03b00944e13dd24ffdbd31810fd2e9f870c9039cfdef37d055053770e9b184508dbd4e80db974512d5018d2384d355871c61 diff --git a/doc/green.mp3 b/doc/green.mp3 new file mode 100644 index 0000000..73e2f0f Binary files /dev/null and b/doc/green.mp3 differ diff --git a/doc/red.mp3 b/doc/red.mp3 new file mode 100644 index 0000000..412e5f4 Binary files /dev/null and b/doc/red.mp3 differ diff --git a/doc/screenshot.gif b/doc/screenshot.gif new file mode 100644 index 0000000..ffd4293 Binary files /dev/null and b/doc/screenshot.gif differ diff --git a/gems.rb b/gems.rb new file mode 100644 index 0000000..be173b2 --- /dev/null +++ b/gems.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec diff --git a/guardfile.rb b/guardfile.rb new file mode 100644 index 0000000..5eb22ed --- /dev/null +++ b/guardfile.rb @@ -0,0 +1,7 @@ +clearing :on + +guard :minitest do + watch(%r{^spec/(.+)_spec\.rb}) + watch(%r{^lib/(.+)\.rb}) { "spec/lib/#{_1[1]}_spec.rb" } + watch(%r{^spec/spec_helper.rb}) { 'spec' } +end diff --git a/lib/minitest/substitute.rb b/lib/minitest/substitute.rb new file mode 100644 index 0000000..c565d7b --- /dev/null +++ b/lib/minitest/substitute.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative 'substitute/version' + +require_relative 'substitute/with' +require_relative 'substitute/spec' + +include Minitest::Substitute::With diff --git a/lib/minitest/substitute/spec.rb b/lib/minitest/substitute/spec.rb new file mode 100644 index 0000000..d7b278f --- /dev/null +++ b/lib/minitest/substitute/spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'minitest/spec' + +Minitest::Spec::DSL.class_eval do + class Substitutor + include Minitest::Substitute::With + end + + # Substitute the variable value for the duration of the current description + # + # @param variable [String] instance or global variable name + # @param substitute [Object] temporary substitution value + # @param on [Object, nil] substitute in the context of this object + # @yield temporary substitution value (takes precedence over +substitute+ param) + def with(variable, substitute=nil, on: self) + before do + substitute = yield if block_given? + Substitutor.send(:commit_substitution, variable, substitute, on: on) + end + after do + Substitutor.send(:rollback_substitution, variable, on: on) + end + end + + # Minitest does not support multiple before/after blocks + remove_method :before + def before(_type=nil, &block) + include(Module.new do + define_method(:setup) do + super() + instance_eval &block + end + end) # .then &:include + end + + remove_method :after + def after(_type=nil, &block) + include(Module.new do + define_method(:teardown) do + instance_eval &block + ensure + super() + end + end) #.then &:include + end +end diff --git a/lib/minitest/substitute/version.rb b/lib/minitest/substitute/version.rb new file mode 100644 index 0000000..c15c281 --- /dev/null +++ b/lib/minitest/substitute/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Minitest + module Substitute + VERSION = '0.1.0' + end +end diff --git a/lib/minitest/substitute/with.rb b/lib/minitest/substitute/with.rb new file mode 100644 index 0000000..0d02032 --- /dev/null +++ b/lib/minitest/substitute/with.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Minitest + module Substitute + module With + class << self + attr_accessor :original + end + + # Substitute the variable value for the duration of the given block + # + # @param variable [String] instance or global variable name + # @param substitute [Object] temporary substitution value + # @param on [Object, nil] substitute in the context of this object + # @yield block during which the substitution is made + # @return [Object] return value of the yielded block + def with(variable, substitute, on: self) + commit_substitution(variable, substitute, on: on) + yield.tap do + rollback_substitution(variable, on: on) + end + end + + private + + def commit_substitution(variable, substitute, on:) + Minitest::Substitute::With.original = on.instance_eval variable.to_s + on.instance_eval "#{variable} = substitute" + end + + def rollback_substitution(variable, on:) + on.instance_eval "#{variable} = Minitest::Substitute::With.original" + end + end + end +end diff --git a/minitest-substitute.gemspec b/minitest-substitute.gemspec new file mode 100644 index 0000000..320fc4d --- /dev/null +++ b/minitest-substitute.gemspec @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative 'lib/minitest/substitute/version' + +Gem::Specification.new do |spec| + spec.name = 'minitest-substitute' + spec.version = Minitest::Substitute::VERSION + spec.summary = 'Substitute values for the duration of a block or a group of tests' + spec.description = <<~END + END + spec.authors = ['Sven Schwyn'] + spec.email = ['ruby@bitcetera.com'] + spec.homepage = 'https://github.com/svoop/minitest-substitute' + spec.license = 'MIT' + + spec.metadata = { + 'homepage_uri' => spec.homepage, + 'changelog_uri' => 'https://github.com/svoop/minitest-substitute/blob/main/CHANGELOG.md', + 'source_code_uri' => 'https://github.com/svoop/minitest-substitute', + 'documentation_uri' => 'https://www.rubydoc.info/gems/minitest-substitute', + 'bug_tracker_uri' => 'https://github.com/svoop/minitest-substitute/issues' + } + + spec.files = Dir['lib/**/*'] + spec.require_paths = %w(lib) + + spec.cert_chain = ["certs/svoop.pem"] + spec.signing_key = File.expand_path(ENV['GEM_SIGNING_KEY']) if ENV['GEM_SIGNING_KEY'] + + spec.extra_rdoc_files = Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt'] + spec.rdoc_options += [ + '--title', 'Minitest::Substitute', + '--main', 'README.md', + '--line-numbers', + '--inline-source', + '--quiet' + ] + + spec.required_ruby_version = '>= 3.0.0' + + spec.add_runtime_dependency 'minitest', '~> 5' + + spec.add_development_dependency 'debug' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'minitest' + spec.add_development_dependency 'minitest-flash' + spec.add_development_dependency 'minitest-focus' + spec.add_development_dependency 'guard' + spec.add_development_dependency 'guard-minitest' + spec.add_development_dependency 'yard' +end diff --git a/rakefile.rb b/rakefile.rb new file mode 100644 index 0000000..48fe8d3 --- /dev/null +++ b/rakefile.rb @@ -0,0 +1,30 @@ +require 'bundler/gem_tasks' + +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs << 'lib' + t.test_files = FileList['spec/lib/**/*_spec.rb'] + t.verbose = false + t.warning = !ENV['RUBYOPT']&.match?(/-W0/) +end + +namespace :yard do + desc "Run local YARD documentation server" + task :server do + `rm -rf ./.yardoc` + Thread.new do + sleep 2 + `open http://localhost:8808` + end + `yard server -r` + end +end + +Rake::Task[:test].enhance do + if ENV['RUBYOPT']&.match?(/-W0/) + puts "⚠️ Ruby warnings are disabled, remove -W0 from RUBYOPT to enable." + end +end + +task default: :test diff --git a/spec/factory.rb b/spec/factory.rb new file mode 100644 index 0000000..3a9e8d8 --- /dev/null +++ b/spec/factory.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Config + def initialize + @version = 1 + end +end + +$_spec_config_instance = Config.new +$_spec_global_variable = :original diff --git a/spec/lib/minitest/substitute/spec_spec.rb b/spec/lib/minitest/substitute/spec_spec.rb new file mode 100644 index 0000000..d22ee3c --- /dev/null +++ b/spec/lib/minitest/substitute/spec_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative '../../../spec_helper' +require_relative '../../../factory' + +describe :with do + 10.times do # test value reset even though order is random + describe "instance variable" do + context 'untouched' do + it "returns the original value" do + _($_spec_config_instance.instance_variable_get(:@version)).must_equal 1 + end + end + + context 'substituted' do + with '@version', 2, on: $_spec_config_instance + + it "returns the substitute value" do + _($_spec_config_instance.instance_variable_get(:@version)).must_equal 2 + end + end + end + end + + 10.times do # test value reset even though order is random + describe "global variable" do + context 'untouched' do + it "returns the original value" do + _($_spec_global_variable).must_equal :original + end + end + + context 'substituted' do + with "$_spec_global_variable", 'oggy' + + it "returns the substitute value" do + _($_spec_global_variable).must_equal 'oggy' + end + end + end + end + + 10.times do # test value reset even though order is random + describe "environment variable" do + context 'untouched' do + it "returns the original value" do + _(ENV['WITH_SPEC_ENV_VAR']).must_be :nil? + end + end + + context 'substituted' do + with "ENV['WITH_SPEC_ENV_VAR']", 'foobar' + + it "returns the substitute value" do + _(ENV['WITH_SPEC_ENV_VAR']).must_equal 'foobar' + end + end + end + end +end diff --git a/spec/lib/minitest/substitute/version_spec.rb b/spec/lib/minitest/substitute/version_spec.rb new file mode 100644 index 0000000..a8773bf --- /dev/null +++ b/spec/lib/minitest/substitute/version_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative '../../../spec_helper' + +describe Minitest::Substitute::VERSION do + it "must be defined" do + _(Minitest::Substitute::VERSION).wont_be_nil + end +end diff --git a/spec/lib/minitest/substitute/with_spec.rb b/spec/lib/minitest/substitute/with_spec.rb new file mode 100644 index 0000000..d9073e6 --- /dev/null +++ b/spec/lib/minitest/substitute/with_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative '../../../spec_helper' +require_relative '../../../factory' + +describe :with do + describe "instance variable" do + it "substitutes the value for the duration of a block" do + _($_spec_config_instance.instance_variable_get(:@version)).must_equal 1 + with '@version', 2, on: $_spec_config_instance do + _($_spec_config_instance.instance_variable_get(:@version)).must_equal 2 + end + _($_spec_config_instance.instance_variable_get(:@version)).must_equal 1 + end + end + + describe "global variable" do + it "substitutes the value for the duration of a block" do + _($_spec_global_variable).must_equal :original + with "$_spec_global_variable", 'oggy' do + _($_spec_global_variable).must_equal 'oggy' + end + _($_spec_global_variable).must_equal :original + end + end + + describe "environment variable" do + it "substitutes the value for the duration of a block" do + _(ENV['WITH_SPEC_ENV_VAR']).must_be :nil? + with "ENV['WITH_SPEC_ENV_VAR']", 'foobar' do + _(ENV['WITH_SPEC_ENV_VAR']).must_equal 'foobar' + end + _(ENV['WITH_SPEC_ENV_VAR']).must_be :nil? + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..6565587 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +gem 'minitest' + +require 'pathname' +require 'debug' + +require 'minitest/autorun' +require Pathname(__dir__).join('..', 'lib', 'minitest', 'substitute') + +require 'minitest/focus' + +class Minitest::Spec + class << self + alias_method :context, :describe + end +end