├── spec ├── fixtures │ ├── 02_empty_testcase │ │ ├── out.rb │ │ └── in.rb │ ├── 07_assert_match │ │ ├── out.rb │ │ └── in.rb │ ├── 29_assert_empty │ │ ├── out.rb │ │ └── in.rb │ ├── 18_assert_not_equal │ │ ├── out.rb │ │ └── in.rb │ ├── 30_assert_kind_of │ │ ├── out.rb │ │ └── in.rb │ ├── 20_draper_test_case │ │ ├── out.rb │ │ └── in.rb │ ├── 17_expects │ │ ├── in.rb │ │ └── out.rb │ ├── 04_assert_equal │ │ ├── out.rb │ │ └── in.rb │ ├── 31_assert_instance_of │ │ ├── out.rb │ │ └── in.rb │ ├── 05_refute_equal │ │ ├── out.rb │ │ └── in.rb │ ├── 08_assert_nil │ │ ├── in.rb │ │ └── out.rb │ ├── 10_assert_difference │ │ ├── out.rb │ │ └── in.rb │ ├── 19_any_instance │ │ ├── in.rb │ │ └── out.rb │ ├── 06_assert_difference_by │ │ ├── out.rb │ │ └── in.rb │ ├── 22_def_test │ │ ├── out.rb │ │ └── in.rb │ ├── 12_assert_nothing_raised │ │ ├── out.rb │ │ └── in.rb │ ├── 14_stubs │ │ ├── out.rb │ │ └── in.rb │ ├── 28_test_unit_test_case │ │ ├── out.rb │ │ └── in.rb │ ├── 11_assert_raises │ │ ├── out.rb │ │ └── in.rb │ ├── 09_assert_no_difference │ │ ├── out.rb │ │ └── in.rb │ ├── 03_trivial_refutation │ │ ├── in.rb │ │ └── out.rb │ ├── 01_trivial_assertion │ │ ├── in.rb │ │ └── out.rb │ ├── 25_raise_namespace │ │ ├── out.rb │ │ └── in.rb │ ├── 16_stabby_lambda │ │ ├── in.rb │ │ └── out.rb │ ├── 24_shoulda_context │ │ ├── out.rb │ │ └── in.rb │ ├── 23_ctrl_test_two_mth │ │ ├── out.rb │ │ └── in.rb │ ├── 13_setup_teardown │ │ ├── out.rb │ │ └── in.rb │ ├── 27_setup_as_method │ │ ├── out.rb │ │ └── in.rb │ ├── 15_controller │ │ ├── out.rb │ │ └── in.rb │ ├── 26_refute │ │ ├── out.rb │ │ └── in.rb │ └── 21_mocha_once_twice │ │ ├── in.rb │ │ └── out.rb ├── spec_helper.rb ├── minitest_to_rspec_spec.rb └── lib │ ├── errors_spec.rb │ ├── input │ ├── subprocessors │ │ ├── base_spec.rb │ │ ├── defn_spec.rb │ │ ├── iter_spec.rb │ │ ├── klass_spec.rb │ │ └── call_spec.rb │ └── model │ │ ├── defn_spec.rb │ │ └── call_spec.rb │ ├── sexp_assertions_spec.rb │ ├── converter_spec.rb │ └── cli_spec.rb ├── doc ├── rspec.md ├── minitest.md ├── mocha │ └── mocha_test.rb └── supported_assertions.md ├── bin ├── mt2rspec └── console ├── Gemfile ├── .gitignore ├── .travis.yml ├── lib ├── minitest_to_rspec │ ├── version.rb │ ├── input │ │ ├── model │ │ │ ├── base.rb │ │ │ ├── hash_exp.rb │ │ │ ├── defn.rb │ │ │ ├── iter.rb │ │ │ ├── klass.rb │ │ │ └── call.rb │ │ ├── processor.rb │ │ └── subprocessors │ │ │ ├── defn.rb │ │ │ ├── klass.rb │ │ │ ├── base.rb │ │ │ ├── iter.rb │ │ │ └── call.rb │ ├── sexp_assertions.rb │ ├── type.rb │ ├── errors.rb │ ├── converter.rb │ ├── rspec │ │ └── stub.rb │ ├── minitest │ │ └── stub.rb │ └── cli.rb └── minitest_to_rspec.rb ├── Rakefile ├── LICENSE.txt ├── minitest_to_rspec.gemspec ├── CODE_OF_CONDUCT.md ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md └── README.md /spec/fixtures/02_empty_testcase/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Banana) { } 3 | -------------------------------------------------------------------------------- /spec/fixtures/07_assert_match/out.rb: -------------------------------------------------------------------------------- 1 | it("banana matches nana") { expect("banana").to(match(/nana\Z/)) } -------------------------------------------------------------------------------- /spec/fixtures/29_assert_empty/out.rb: -------------------------------------------------------------------------------- 1 | expect(Banana.new).to(be_empty) 2 | expect(Banana.new).to(be_empty) 3 | -------------------------------------------------------------------------------- /spec/fixtures/07_assert_match/in.rb: -------------------------------------------------------------------------------- 1 | test "banana matches nana" do 2 | assert_match /nana\Z/, "banana" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/18_assert_not_equal/out.rb: -------------------------------------------------------------------------------- 1 | expect(:kiwi).to_not(eq(:banana)) 2 | expect(:kiwi).to_not(eq(:banana)) 3 | -------------------------------------------------------------------------------- /spec/fixtures/30_assert_kind_of/out.rb: -------------------------------------------------------------------------------- 1 | expect(Banana.new).to(be_a(Banana)) 2 | expect(Banana.new).to(be_a(Banana)) 3 | -------------------------------------------------------------------------------- /doc/rspec.md: -------------------------------------------------------------------------------- 1 | RSpec 2 | ===== 3 | 4 | Gem | https://rubygems.org/gems/rspec 5 | Docs | https://relishapp.com/rspec 6 | -------------------------------------------------------------------------------- /spec/fixtures/20_draper_test_case/out.rb: -------------------------------------------------------------------------------- 1 | module Fruit 2 | RSpec.describe(BananaDecorator, :type => :decorator) { } 3 | end -------------------------------------------------------------------------------- /spec/fixtures/17_expects/in.rb: -------------------------------------------------------------------------------- 1 | Banana.expects(edible: true, color: "yellow") 2 | Banana.expects(:delicious?).returns(true) 3 | -------------------------------------------------------------------------------- /spec/fixtures/20_draper_test_case/in.rb: -------------------------------------------------------------------------------- 1 | module Fruit 2 | class BananaDecoratorTest < Draper::TestCase 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/02_empty_testcase/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananaTest < ActiveSupport::TestCase 4 | # empty 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/04_assert_equal/out.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe(Kiwi) do 2 | it("is not delicious") { expect(Kiwi.new.delicious?).to(eq(false)) } 3 | end -------------------------------------------------------------------------------- /spec/fixtures/31_assert_instance_of/out.rb: -------------------------------------------------------------------------------- 1 | expect(Banana.new).to(be_instance_of(Banana)) 2 | expect(Banana.new).to(be_instance_of(Banana)) 3 | -------------------------------------------------------------------------------- /spec/fixtures/05_refute_equal/out.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe(Kiwi) do 2 | it("is not delicious") { expect(Kiwi.new.delicious?).to_not(eq(true)) } 3 | end -------------------------------------------------------------------------------- /spec/fixtures/08_assert_nil/in.rb: -------------------------------------------------------------------------------- 1 | test "bananas are delicious, kiwis are gross" do 2 | assert_not_nil banana 3 | assert_nil kiwi 4 | end 5 | -------------------------------------------------------------------------------- /bin/mt2rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'minitest_to_rspec/cli' 5 | MinitestToRspec::CLI.new(ARGV).run 6 | -------------------------------------------------------------------------------- /spec/fixtures/10_assert_difference/out.rb: -------------------------------------------------------------------------------- 1 | it("changes length") do 2 | ary = [] 3 | expect { ary.push("banana") }.to(change { ary.length }) 4 | end -------------------------------------------------------------------------------- /spec/fixtures/18_assert_not_equal/in.rb: -------------------------------------------------------------------------------- 1 | assert_not_equal(:banana, :kiwi) 2 | assert_not_equal(:banana, :kiwi, "Expected kiwi not to equal banana") 3 | -------------------------------------------------------------------------------- /spec/fixtures/19_any_instance/in.rb: -------------------------------------------------------------------------------- 1 | Banana.any_instance.stubs(:delicious?).returns(true) 2 | Kiwi.any_instance.expects(:delicious?).returns(false) 3 | -------------------------------------------------------------------------------- /spec/fixtures/29_assert_empty/in.rb: -------------------------------------------------------------------------------- 1 | assert_empty(Banana.new) 2 | assert_empty(Banana.new, "Expected empty banana skin but got delicious content") 3 | -------------------------------------------------------------------------------- /spec/fixtures/06_assert_difference_by/out.rb: -------------------------------------------------------------------------------- 1 | it("changes length") do 2 | ary = [] 3 | expect { ary.push("banana") }.to(change { ary.length }.by(1)) 4 | end -------------------------------------------------------------------------------- /spec/fixtures/08_assert_nil/out.rb: -------------------------------------------------------------------------------- 1 | it("bananas are delicious, kiwis are gross") do 2 | expect(banana).to_not(be_nil) 3 | expect(kiwi).to(be_nil) 4 | end -------------------------------------------------------------------------------- /spec/fixtures/30_assert_kind_of/in.rb: -------------------------------------------------------------------------------- 1 | assert_kind_of(Banana, Banana.new) 2 | assert_kind_of(Banana, Banana.new, "Expected Banana to be a kind of Banana") 3 | -------------------------------------------------------------------------------- /spec/fixtures/22_def_test/out.rb: -------------------------------------------------------------------------------- 1 | it("blah") { expect(nil).to(eq(nil)) } 2 | it("blah again") do 3 | expect(1).to(eq(1)) 4 | expect(nil).to(eq(nil)) 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in minitest_to_rspec.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /spec/fixtures/12_assert_nothing_raised/out.rb: -------------------------------------------------------------------------------- 1 | describe("Banana.delicious!") do 2 | it("nothing raised") { expect { Banana.delicious! }.to_not(raise_error) } 3 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | .ruby-version 3 | /.bundle/ 4 | /.yardoc 5 | /Gemfile.lock 6 | /_yardoc/ 7 | /coverage/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /spec/fixtures/10_assert_difference/in.rb: -------------------------------------------------------------------------------- 1 | test "changes length" do 2 | ary = [] 3 | assert_difference "ary.length" do 4 | ary.push("banana") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/31_assert_instance_of/in.rb: -------------------------------------------------------------------------------- 1 | assert_instance_of(Banana, Banana.new) 2 | assert_instance_of(Banana, Banana.new, "Expected Banana to be an actual Banana") 3 | -------------------------------------------------------------------------------- /spec/fixtures/05_refute_equal/in.rb: -------------------------------------------------------------------------------- 1 | class KiwiTest < ActiveSupport::TestCase 2 | test "is not delicious" do 3 | refute_equal true, Kiwi.new.delicious? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/06_assert_difference_by/in.rb: -------------------------------------------------------------------------------- 1 | test "changes length" do 2 | ary = [] 3 | assert_difference "ary.length", +1 do 4 | ary.push("banana") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/14_stubs/out.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe(Banana) do 2 | it("random stuff from mocha") do 3 | allow(Banana).to(receive(:delicious?).and_return(true)) 4 | end 5 | end -------------------------------------------------------------------------------- /spec/fixtures/22_def_test/in.rb: -------------------------------------------------------------------------------- 1 | def test_blah 2 | assert_equal nil, nil 3 | end 4 | 5 | def test_blah_again 6 | assert_equal 1, 1 7 | assert_equal nil, nil 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/28_test_unit_test_case/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Banana) do 3 | it("delicious") { expect(Banana.new.delicious?).to(eq(true)) } 4 | end 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 4 | require 'minitest_to_rspec' 5 | require 'byebug' 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.3.6" 4 | - "2.4.3" 5 | - "2.5.0" 6 | script: bundle exec rake 7 | sudo: false 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /spec/fixtures/04_assert_equal/in.rb: -------------------------------------------------------------------------------- 1 | class KiwiTest < ActiveSupport::TestCase 2 | test "is not delicious" do 3 | assert_equal false, Kiwi.new.delicious? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/14_stubs/in.rb: -------------------------------------------------------------------------------- 1 | class BananaTest < ActiveSupport::TestCase 2 | test "random stuff from mocha" do 3 | Banana.stubs(:delicious?).returns(true) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'minitest_to_rspec' 6 | require 'byebug' 7 | require 'irb' 8 | IRB.start 9 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #:nodoc: 4 | module MinitestToRspec 5 | def self.gem_version 6 | ::Gem::Version.new('0.13.0') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/19_any_instance/out.rb: -------------------------------------------------------------------------------- 1 | allow_any_instance_of(Banana).to(receive(:delicious?).and_return(true)) 2 | expect_any_instance_of(Kiwi).to(receive(:delicious?).and_return(false).once) 3 | -------------------------------------------------------------------------------- /spec/fixtures/11_assert_raises/out.rb: -------------------------------------------------------------------------------- 1 | describe("Kiwi.delicious!") do 2 | it("raises NotDeliciousError") do 3 | expect { Kiwi.delicious! }.to(raise_error(NotDeliciousError)) 4 | end 5 | end -------------------------------------------------------------------------------- /spec/fixtures/28_test_unit_test_case/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananaTest < Test::Unit::TestCase 4 | def test_delicious 5 | assert Banana.new.delicious? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/12_assert_nothing_raised/in.rb: -------------------------------------------------------------------------------- 1 | describe "Banana.delicious!" do 2 | test "nothing raised" do 3 | assert_nothing_raised do 4 | Banana.delicious! 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/09_assert_no_difference/out.rb: -------------------------------------------------------------------------------- 1 | describe("#peel") do 2 | it("does not change flavor") do 3 | banana = Banana.new 4 | expect { banana.peel }.to_not(change { banana.flavor }) 5 | end 6 | end -------------------------------------------------------------------------------- /spec/fixtures/11_assert_raises/in.rb: -------------------------------------------------------------------------------- 1 | describe "Kiwi.delicious!" do 2 | test "raises NotDeliciousError" do 3 | assert_raises(NotDeliciousError) do 4 | Kiwi.delicious! 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/03_trivial_refutation/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class KiwiTest < ActiveSupport::TestCase 4 | test "is not delicious" do 5 | refute Kiwi 6 | refute Kiwi.new.delicious? 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/01_trivial_assertion/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananaTest < ActiveSupport::TestCase 4 | test "is delicious" do 5 | assert Banana 6 | assert Banana.new.delicious? 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/25_raise_namespace/out.rb: -------------------------------------------------------------------------------- 1 | describe("Kiwi.delicious!") do 2 | it("raises Namespace::NotDeliciousError") do 3 | expect { Kiwi.delicious! }.to(raise_error(Namespace::NotDeliciousError)) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/01_trivial_assertion/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Banana) do 3 | it("is delicious") do 4 | expect(Banana).to(be_truthy) 5 | expect(Banana.new.delicious?).to(eq(true)) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/03_trivial_refutation/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Kiwi) do 3 | it("is not delicious") do 4 | expect(Kiwi).to(be_falsey) 5 | expect(Kiwi.new.delicious?).to(eq(false)) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/16_stabby_lambda/in.rb: -------------------------------------------------------------------------------- 1 | test "doing something should not change Banana count" do 2 | λ = -> { Banana.count } 3 | before = λ.call 4 | do_something 5 | after = λ.call 6 | assert_equal before, after 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/16_stabby_lambda/out.rb: -------------------------------------------------------------------------------- 1 | it("doing something should not change Banana count") do 2 | λ = lambda { Banana.count } 3 | before = λ.call 4 | do_something 5 | after = λ.call 6 | expect(after).to(eq(before)) 7 | end -------------------------------------------------------------------------------- /spec/fixtures/09_assert_no_difference/in.rb: -------------------------------------------------------------------------------- 1 | describe "#peel" do 2 | test "does not change flavor" do 3 | banana = Banana.new 4 | assert_no_difference "banana.flavor" do 5 | banana.peel 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/24_shoulda_context/out.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe(Foo) do 2 | context("in a world run by monkeys") do 3 | before { bananas } 4 | it("be worth more than gold") { } 5 | it("be delicious") { } 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/25_raise_namespace/in.rb: -------------------------------------------------------------------------------- 1 | describe "Kiwi.delicious!" do 2 | test "raises Namespace::NotDeliciousError" do 3 | assert_raises(Namespace::NotDeliciousError) do 4 | Kiwi.delicious! 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/23_ctrl_test_two_mth/out.rb: -------------------------------------------------------------------------------- 1 | require("rails_helper") 2 | RSpec.describe(MyController, :type => :controller) do 3 | it("herp") { nil } 4 | it("derp") { nil } 5 | def do_not_convert_me 6 | # do nothing 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/version' 4 | require 'minitest_to_rspec/converter' 5 | 6 | # A command-line tool for converting minitest files to rspec. 7 | module MinitestToRspec 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/23_ctrl_test_two_mth/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MyControllerTest < ActionController::TestCase 4 | def test_herp 5 | end 6 | 7 | def test_derp 8 | end 9 | 10 | def do_not_convert_me 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/13_setup_teardown/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Banana) do 3 | include(Monkeys) 4 | before { fend_off_the_monkeys } 5 | it("is delicious") { expect(Banana.new.delicious?).to(eq(true)) } 6 | after { appologize_to_the_monkeys } 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/27_setup_as_method/out.rb: -------------------------------------------------------------------------------- 1 | require("spec_helper") 2 | RSpec.describe(Banana) do 3 | include(Monkeys) 4 | before { fend_off_the_monkeys } 5 | it("is delicious") { expect(Banana.new.delicious?).to(eq(true)) } 6 | after { appologize_to_the_monkeys } 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/15_controller/out.rb: -------------------------------------------------------------------------------- 1 | require("rails_helper") 2 | RSpec.describe(BananasController, :type => :controller) do 3 | include(BananaSeeds) 4 | before { scare_away_monkeys } 5 | it("index") do 6 | get(:index) 7 | assert_response(:success) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/17_expects/out.rb: -------------------------------------------------------------------------------- 1 | lambda do 2 | "Sorry for the pointless lambda here." 3 | expect(Banana).to(receive(:edible).and_return(true).once) 4 | expect(Banana).to(receive(:color).and_return("yellow").once) 5 | end.call 6 | expect(Banana).to(receive(:delicious?).and_return(true).once) 7 | -------------------------------------------------------------------------------- /spec/minitest_to_rspec_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe MinitestToRspec do 6 | describe '.gem_version' do 7 | it 'returns a Gem::Version' do 8 | expect(described_class.gem_version).to be_a(::Gem::Version) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/15_controller/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananasControllerTest < ActionController::TestCase 4 | include BananaSeeds 5 | 6 | setup do 7 | scare_away_monkeys 8 | end 9 | 10 | test 'index' do 11 | get :index 12 | assert_response :success 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/24_shoulda_context/in.rb: -------------------------------------------------------------------------------- 1 | class FooTest < ActiveSupport::TestCase 2 | context "in a world run by monkeys" do 3 | setup do 4 | bananas 5 | end 6 | 7 | should "be worth more than gold" do 8 | end 9 | 10 | should "be delicious" do 11 | end 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/13_setup_teardown/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananaTest < ActiveSupport::TestCase 4 | include Monkeys 5 | 6 | setup do 7 | fend_off_the_monkeys 8 | end 9 | 10 | test "is delicious" do 11 | assert Banana.new.delicious? 12 | end 13 | 14 | teardown do 15 | appologize_to_the_monkeys 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/fixtures/27_setup_as_method/in.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BananaTest < ActiveSupport::TestCase 4 | include Monkeys 5 | 6 | def setup 7 | fend_off_the_monkeys 8 | end 9 | 10 | test "is delicious" do 11 | assert Banana.new.delicious? 12 | end 13 | 14 | def teardown 15 | appologize_to_the_monkeys 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/errors_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MinitestToRspec 4 | RSpec.describe ModuleShorthandError do 5 | describe '#message' do 6 | it 'explains that class definition is unsupported' do 7 | expect(described_class.new.to_s).to include( 8 | 'Module shorthand (A::B::C) is not supported' 9 | ) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | require 'rubocop/rake_task' 5 | 6 | RuboCop::RakeTask.new 7 | 8 | task(:spec).clear 9 | RSpec::Core::RakeTask.new(:spec) do |t| 10 | t.verbose = false 11 | end 12 | 13 | # Default task: lint then test 14 | task default: [] # in case it hasn't been set 15 | Rake::Task[:default].clear 16 | task default: %i[rubocop spec] 17 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/sexp_assertions' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Input models are simple data objects 9 | # representing the S-expressions they are named after. 10 | class Base 11 | include SexpAssertions 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/26_refute/out.rb: -------------------------------------------------------------------------------- 1 | describe("Banana.delicious!") do 2 | it("doesn't raise DeliciousError") do 3 | expect { Banana.delicious! }.to_not(raise_error(DeliciousError)) 4 | end 5 | it("doesn't raises DeliciousError") do 6 | expect { Banana.delicious! }.to_not(raise_error(DeliciousError)) 7 | end 8 | it("doesn't raise namespaced DeliciousError") do 9 | expect { Banana.delicious! }.to_not(raise_error(Namespace::DeliciousError)) 10 | end 11 | it("doesn't raises namespaced DeliciousError") do 12 | expect { Banana.delicious! }.to_not(raise_error(Namespace::DeliciousError)) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/26_refute/in.rb: -------------------------------------------------------------------------------- 1 | describe "Banana.delicious!" do 2 | test "doesn't raise DeliciousError" do 3 | refute_raise(DeliciousError) do 4 | Banana.delicious! 5 | end 6 | end 7 | 8 | test "doesn't raises DeliciousError" do 9 | refute_raises(DeliciousError) do 10 | Banana.delicious! 11 | end 12 | end 13 | 14 | test "doesn't raise namespaced DeliciousError" do 15 | refute_raise(Namespace::DeliciousError) do 16 | Banana.delicious! 17 | end 18 | end 19 | 20 | test "doesn't raises namespaced DeliciousError" do 21 | refute_raises(Namespace::DeliciousError) do 22 | Banana.delicious! 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/hash_exp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/model/base' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Data object. Represents a `:hash` S-expression. 9 | class HashExp < Base 10 | def initialize(sexp) 11 | assert_sexp_type(:hash, sexp) 12 | @exp = sexp.dup 13 | end 14 | 15 | # A slightly nicer implementation would be: 16 | # `@exp[1..-1].each_slice(2).to_h` 17 | # but that would require ruby >= 2.1 18 | def to_h 19 | Hash[@exp[1..-1].each_slice(2).to_a] 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/sexp_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MinitestToRspec 4 | # Useful runtime assertions regarding S-expressions. 5 | module SexpAssertions 6 | def assert_sexp_type_array(type, obj) 7 | unless obj.is_a?(Array) && obj.all? { |x| sexp_type?(type, x) } 8 | raise TypeError, "Expected array of #{type} sexp, got #{obj.inspect}" 9 | end 10 | end 11 | 12 | def assert_sexp_type(type, exp) 13 | unless sexp_type?(type, exp) 14 | raise TypeError, "Expected #{type} s-expression, got #{exp.inspect}" 15 | end 16 | end 17 | 18 | def sexp_type?(type, exp) 19 | exp.is_a?(Sexp) && exp.sexp_type == type.to_sym 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/input/subprocessors/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Subprocessors 8 | RSpec.describe Base do 9 | let(:subprocessor) { described_class.new(false, false) } 10 | 11 | describe '#allow_to' do 12 | let(:msg_recipient) { double } 13 | let(:matcher) { double } 14 | 15 | it 'returns a call expression' do 16 | exp = subprocessor.allow_to(msg_recipient, matcher) 17 | expect(exp).to eq( 18 | s(:call, 19 | s(:call, nil, :allow, msg_recipient), 20 | :to, 21 | matcher 22 | ) 23 | ) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /doc/minitest.md: -------------------------------------------------------------------------------- 1 | Minitest 2 | ======== 3 | 4 | Gem | https://rubygems.org/gems/minitest 5 | Docs | http://www.rubydoc.info/gems/minitest/5.5.1 6 | 7 | ActiveSupport 8 | ------------- 9 | 10 | Rails adds some assertions to minitest. 11 | 12 | - http://guides.rubyonrails.org/testing.html 13 | - [active_support/testing/assertions.rb][3] 14 | 15 | Test::Unit 16 | ---------- 17 | 18 | - https://github.com/test-unit/test-unit 19 | - http://www.rubydoc.info/gems/test-unit/3.0.9/Test/Unit/Assertions 20 | 21 | Selected History 22 | ---------------- 23 | 24 | [Minitest was removed from ruby stdlib][1] in ruby 2.2, but it is 25 | still [packaged with ruby][2] somehow. 26 | 27 | [1]: https://bugs.ruby-lang.org/issues/9711 28 | [2]: https://bugs.ruby-lang.org/issues/9852 29 | [3]: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/testing/assertions.rb 30 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/defn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/model/base' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Data object. Represents a `:defn` s-expression. 9 | class Defn < Base 10 | def initialize(exp) 11 | assert_sexp_type(:defn, exp) 12 | @exp = exp.dup 13 | end 14 | 15 | def body 16 | @exp[3..-1] 17 | end 18 | 19 | def method_name 20 | @exp[1].to_s 21 | end 22 | 23 | def test_method? 24 | method_name.start_with?('test_') 25 | end 26 | 27 | def setup? 28 | method_name == 'setup' 29 | end 30 | 31 | def teardown? 32 | method_name == 'teardown' 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MinitestToRspec 4 | # Runtime type assertions. 5 | module Type 6 | class << self 7 | def assert(types, value) 8 | unless array_wrap(types).any? { |t| value.is_a?(t) } 9 | raise TypeError, "Expected #{types}, got #{value}" 10 | end 11 | end 12 | 13 | def bool(value) 14 | unless [false, true].include?(value) 15 | raise TypeError, "Expected Boolean, got #{value}" 16 | end 17 | end 18 | 19 | private 20 | 21 | # Implementation copied from Array.wrap in ActiveSupport 5 22 | def array_wrap(object) 23 | if object.nil? 24 | [] 25 | elsif object.respond_to?(:to_ary) 26 | object.to_ary || [object] 27 | else 28 | [object] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/fixtures/21_mocha_once_twice/in.rb: -------------------------------------------------------------------------------- 1 | a.stubs(:x).once 2 | a.stubs(:x).twice 3 | a.stubs(:x).returns(1).once 4 | a.stubs(:x).returns(1).twice 5 | a.stubs(:x).with(:y).returns(1).once 6 | a.stubs(:x).with(:y).returns(1).twice 7 | a.expects(:x).once 8 | a.expects(:x).twice 9 | a.expects(:x).returns(1).once 10 | a.expects(:x).returns(1).twice 11 | a.expects(:x).with(:y).returns(1).once 12 | a.expects(:x).with(:y).returns(1).twice 13 | A.any_instance.stubs(:x).once 14 | A.any_instance.stubs(:x).twice 15 | A.any_instance.stubs(:x).returns(1).once 16 | A.any_instance.stubs(:x).returns(1).twice 17 | A.any_instance.expects(:x).returns(1).once 18 | A.any_instance.expects(:x).returns(1).twice 19 | A.any_instance.stubs(:x).with(:y).returns(1).once 20 | A.any_instance.stubs(:x).with(:y).returns(1).twice 21 | A.any_instance.expects(:x).with(:y).returns(1).once 22 | A.any_instance.expects(:x).with(:y).returns(1).twice 23 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MinitestToRspec 4 | class Error < StandardError; end 5 | class ProcessingError < Error; end 6 | 7 | # Raise `UnknownVariant` to indicate that an expression is 8 | # not recognized. This exception should always be rescued, 9 | # and the original expression should be used in the output. 10 | class UnknownVariant < Error; end 11 | 12 | # Raise `NotImplemented` to indicate that an expression is 13 | # recognized (not an `UnknownVariant`) but that `minitest_to_rspec` 14 | # does not (yet) implement a conversion. 15 | class NotImplemented < Error; end 16 | 17 | # See DEFAULT_MESSAGE 18 | class ModuleShorthandError < NotImplemented 19 | DEFAULT_MESSAGE = <<~EOS 20 | Unsupported class definition: Module shorthand (A::B::C) is not supported. 21 | Please convert your class definition to use nested modules and try again. 22 | EOS 23 | 24 | def initialize(msg = nil) 25 | super(msg || DEFAULT_MESSAGE) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jared Beck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/mocha/mocha_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | gem 'mocha' 5 | require 'minitest/unit' 6 | require 'mocha/mini_test' 7 | require 'minitest/autorun' 8 | 9 | # Reverse engineering mocha, to clarify some things missing in the mocha docs. 10 | class TestDerp < Minitest::Test 11 | def test_expects_return_value 12 | x = Object.new 13 | x.define_singleton_method(:y) { :z } # this won't get called 14 | x.expects(:y) 15 | assert_nil x.y 16 | # passes 17 | end 18 | 19 | def test_stubs_only 20 | x = Object.new 21 | x.stubs(:y) 22 | # passes 23 | end 24 | 25 | def test_stubs_returns 26 | x = Object.new 27 | x.stubs(:y).returns(:z) 28 | # passes 29 | end 30 | 31 | def test_stubs_once 32 | x = Object.new 33 | x.stubs(:y).once 34 | # fails: expected exactly once, not yet invoked 35 | end 36 | 37 | def test_stubs_twice 38 | x = Object.new 39 | x.stubs(:y).twice 40 | # fails: expected exactly twice, not yet invoked 41 | end 42 | 43 | def test_stubs_return_value 44 | x = Object.new 45 | x.define_singleton_method(:y) { :z } # this won't get called 46 | x.stubs(:y) 47 | assert_nil x.y 48 | # passes 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby_parser' 4 | require 'sexp_processor' 5 | require_relative 'subprocessors/call' 6 | require_relative 'subprocessors/defn' 7 | require_relative 'subprocessors/klass' 8 | require_relative 'subprocessors/iter' 9 | 10 | module MinitestToRspec 11 | module Input 12 | # Consumes a `String` of minitest code and returns an S-expression 13 | # representing equivalent RSpec code. The main method is `#process`. See 14 | # `SexpProcessor` docs for details. 15 | class Processor < SexpProcessor 16 | def initialize(rails, mocha) 17 | super() 18 | self.strict = false 19 | @mocha = mocha 20 | @rails = rails 21 | end 22 | 23 | def process_call(exp) 24 | Subprocessors::Call.new(exp, @rails, @mocha).process 25 | end 26 | 27 | def process_class(exp) 28 | Subprocessors::Klass.new(exp, @rails, @mocha).process 29 | end 30 | 31 | def process_defn(exp) 32 | Subprocessors::Defn.new(exp, @rails, @mocha).process 33 | end 34 | 35 | def process_iter(exp) 36 | Subprocessors::Iter.new(exp, @rails, @mocha).process 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/fixtures/21_mocha_once_twice/out.rb: -------------------------------------------------------------------------------- 1 | expect(a).to(receive(:x).once) 2 | expect(a).to(receive(:x).twice) 3 | expect(a).to(receive(:x).and_return(1).once) 4 | expect(a).to(receive(:x).and_return(1).twice) 5 | expect(a).to(receive(:x).with(:y).and_return(1).once) 6 | expect(a).to(receive(:x).with(:y).and_return(1).twice) 7 | expect(a).to(receive(:x).once) 8 | expect(a).to(receive(:x).twice) 9 | expect(a).to(receive(:x).and_return(1).once) 10 | expect(a).to(receive(:x).and_return(1).twice) 11 | expect(a).to(receive(:x).with(:y).and_return(1).once) 12 | expect(a).to(receive(:x).with(:y).and_return(1).twice) 13 | expect_any_instance_of(A).to(receive(:x).once) 14 | expect_any_instance_of(A).to(receive(:x).twice) 15 | expect_any_instance_of(A).to(receive(:x).and_return(1).once) 16 | expect_any_instance_of(A).to(receive(:x).and_return(1).twice) 17 | expect_any_instance_of(A).to(receive(:x).and_return(1).once) 18 | expect_any_instance_of(A).to(receive(:x).and_return(1).twice) 19 | expect_any_instance_of(A).to(receive(:x).with(:y).and_return(1).once) 20 | expect_any_instance_of(A).to(receive(:x).with(:y).and_return(1).twice) 21 | expect_any_instance_of(A).to(receive(:x).with(:y).and_return(1).once) 22 | expect_any_instance_of(A).to(receive(:x).with(:y).and_return(1).twice) 23 | -------------------------------------------------------------------------------- /spec/lib/input/model/defn_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'ruby_parser' 5 | 6 | module MinitestToRspec 7 | module Input 8 | module Model 9 | RSpec.describe Defn do 10 | describe '#body' do 11 | it 'returns sexp' do 12 | method = <<-RUBY 13 | def method_name 14 | inner_method 15 | end 16 | RUBY 17 | sexp = RubyParser.new.parse(method) 18 | expect(described_class.new(sexp).body) 19 | .to eq(s(s(:call, nil, :inner_method))) 20 | end 21 | end 22 | 23 | describe '#method_name' do 24 | it 'returns the method name as a string' do 25 | sexp = s(:defn, :method_name) 26 | expect(described_class.new(sexp).method_name).to eq('method_name') 27 | end 28 | end 29 | 30 | describe '#test_method?' do 31 | context 'method name begins with test_' do 32 | it 'returns true' do 33 | sexp = s(:defn, :test_banana) 34 | expect(described_class.new(sexp)).to be_test_method 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/sexp_assertions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module MinitestToRspec 6 | class ClassWithSexpAssertions 7 | extend SexpAssertions 8 | end 9 | 10 | RSpec.describe ClassWithSexpAssertions do 11 | def expect_type_error(message) 12 | expect { yield }.to raise_error(TypeError, message) 13 | end 14 | 15 | describe '.assert_sexp_type' do 16 | context 'nil' do 17 | it 'raises TypeError' do 18 | expect_type_error('Expected derp s-expression, got nil') { 19 | described_class.assert_sexp_type(:derp, nil) 20 | } 21 | end 22 | end 23 | 24 | context 'wrong sexp_type' do 25 | it 'raises TypeError, inspects sexp' do 26 | expect_type_error('Expected foo s-expression, got s(:bar)') { 27 | described_class.assert_sexp_type(:foo, s(:bar)) 28 | } 29 | end 30 | end 31 | end 32 | 33 | describe '.assert_sexp_type_array' do 34 | context 'nil' do 35 | it 'raises TypeError' do 36 | expect_type_error('Expected array of foo sexp, got nil') { 37 | described_class.assert_sexp_type_array(:foo, nil) 38 | } 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /minitest_to_rspec.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'minitest_to_rspec/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'minitest_to_rspec' 9 | spec.version = MinitestToRspec.gem_version.to_s 10 | spec.executables << 'mt2rspec' 11 | spec.authors = ['Jared Beck'] 12 | spec.email = ['jared@jaredbeck.com'] 13 | 14 | spec.summary = 'Converts minitest files to rspec' 15 | spec.description = <<~EOS 16 | A command-line tool for converting minitest files to rspec. 17 | EOS 18 | spec.homepage = 'https://github.com/jaredbeck/minitest_to_rspec' 19 | spec.license = 'MIT' 20 | 21 | spec.files = `git ls-files -z`.split("\x0").reject { |f| 22 | f.match(%r{^(test|spec|features)/}) 23 | } 24 | spec.require_paths = ['lib'] 25 | spec.required_ruby_version = '>= 2.3.0' 26 | 27 | spec.add_runtime_dependency 'optimist', '~> 3.0' 28 | spec.add_runtime_dependency 'ruby2ruby', '~> 2.4.4' 29 | spec.add_runtime_dependency 'ruby_parser', '~> 3.11.0' 30 | 31 | spec.add_development_dependency 'bundler', '~> 2.0' 32 | spec.add_development_dependency 'byebug', '~> 11.0' 33 | spec.add_development_dependency 'rake', '~> 13.0' 34 | spec.add_development_dependency 'rspec', '~> 3.9' 35 | spec.add_development_dependency 'rubocop', '~> 0.79' 36 | end 37 | -------------------------------------------------------------------------------- /doc/supported_assertions.md: -------------------------------------------------------------------------------- 1 | Supported Assertions 2 | ==================== 3 | 4 | A quick survey of one mid-sized test suite (14 kilolines) found 5 | the following assertions in use. 6 | 7 | ``` 8 | 625 assert 9 | 530 assert_equal 10 | 84 assert_difference 11 | 38 assert_no_difference 12 | 10 refute 13 | 10 assert_nil 14 | 5 assert_nothing_raised 15 | 4 assert_match 16 | 2 assert_raises 17 | 1 refute_equal 18 | 0 refute_same 19 | 0 refute_respond_to 20 | 0 refute_predicate 21 | 0 refute_operator 22 | 0 refute_nil 23 | 0 refute_match 24 | 0 refute_kind_of 25 | 0 refute_instance_of 26 | 0 refute_includes 27 | 0 refute_in_epsilon 28 | 0 refute_in_delta 29 | 0 refute_empty 30 | 0 assert_throws 31 | 0 assert_silent 32 | 0 assert_send 33 | 0 assert_same 34 | 0 assert_respond_to 35 | 0 assert_present 36 | 0 assert_predicate 37 | 0 assert_output 38 | 0 assert_operator 39 | 0 assert_not 40 | 0 assert_kind_of 41 | 0 assert_instance_of 42 | 0 assert_includes 43 | 0 assert_in_epsilon 44 | 0 assert_in_delta 45 | 0 assert_empty 46 | 0 assert_blank 47 | ``` 48 | 49 | Assertions which are not used in the targeted test suite 50 | are not yet supported, but contributions are welcome. 51 | 52 | A Script to Count Assertions 53 | ---------------------------- 54 | 55 | ```bash 56 | find test -type f -name '*.rb' | xargs cat > all_tests; 57 | for a in $( cat ~/Desktop/assertions ); do 58 | echo -n $a 59 | ggrep -E "\\b$a\\b" all_tests | wc -l 60 | done | 61 | awk '{print $2 " " $1}' | 62 | sort -nr 63 | ``` 64 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby_parser' 4 | require 'ruby2ruby' 5 | require 'minitest_to_rspec/input/processor' 6 | require_relative 'errors' 7 | 8 | module MinitestToRspec 9 | # Converts strings of minitest code. Does not read or write files. 10 | class Converter 11 | def initialize(rails: false, mocha: false) 12 | @processor = Input::Processor.new(rails, mocha) 13 | end 14 | 15 | # - `input` - Contents of a ruby file. 16 | # - `file_path` - Optional. Value will replace any `__FILE__` 17 | # keywords in the input. 18 | def convert(input, file_path = nil) 19 | render process parse(input, file_path) 20 | end 21 | 22 | private 23 | 24 | # Parses input string and returns Abstract Syntax Tree (AST) 25 | # as an S-expression. 26 | def parse(input, file_path) 27 | file_path ||= "No file path provided to #{self.class}#convert" 28 | RubyParser.new.parse(input, file_path) 29 | end 30 | 31 | # Processes an AST (S-expressions) representing a minitest 32 | # file, and returns an AST (still S-expressions) representing 33 | # an rspec file. 34 | def process(exp) 35 | @processor.process(exp) 36 | end 37 | 38 | # Given an AST representing an rspec file, returns a string 39 | # of ruby code. 40 | def render(exp) 41 | renderer.process(exp) 42 | end 43 | 44 | def renderer 45 | Ruby2Ruby.new 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/input/model/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | RSpec.describe Call do 9 | def parse(exp) 10 | RubyParser.new.parse(exp) 11 | end 12 | 13 | describe '.new' do 14 | context 'sexp_type is not :call' do 15 | it 'raises TypeError' do 16 | expect { 17 | described_class.new(s(:str, 'derp')) 18 | }.to raise_error(TypeError) 19 | end 20 | end 21 | end 22 | 23 | describe '#receiver_chain' do 24 | def receiver_chain(exp) 25 | described_class.new(exp).receiver_chain 26 | end 27 | 28 | it 'returns array' do 29 | expect(receiver_chain(parse('@a.b.c'))).to eq( 30 | [parse('@a.b'), parse('@a')] 31 | ) 32 | end 33 | 34 | it 'returns array' do 35 | expect(receiver_chain(parse('@a.b(1, :x).c(2, 3)'))).to eq( 36 | [parse('@a.b(1, :x)'), parse('@a')] 37 | ) 38 | end 39 | 40 | context 'when leaf sexp is a call with nil receiver' do 41 | it 'includes the nil receiver' do 42 | expect(receiver_chain(parse('a.b.c'))).to eq( 43 | [parse('a.b'), parse('a'), nil] 44 | ) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people 4 | who contribute through reporting issues, posting feature requests, updating 5 | documentation, submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free 8 | experience for everyone, regardless of level of experience, gender, gender 9 | identity and expression, sexual orientation, disability, personal appearance, 10 | body size, race, age, or religion. 11 | 12 | Examples of unacceptable behavior by participants include the use of sexual 13 | language or imagery, derogatory comments or personal attacks, trolling, public 14 | or private harassment, insults, or other unprofessional conduct. 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject 17 | comments, commits, code, wiki edits, issues, and other contributions that are 18 | not aligned to this Code of Conduct. Project maintainers who do not follow the 19 | Code of Conduct may be removed from the project team. 20 | 21 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 22 | reported by opening an issue or contacting one or more of the project 23 | maintainers. 24 | 25 | This Code of Conduct is adapted from the [Contributor 26 | Covenant](http:contributor-covenant.org), version 1.0.0, available at 27 | [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org 28 | /version/1/0/0/) 29 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | - spec/fixtures/**/* 6 | TargetRubyVersion: 2.3 7 | 8 | Layout/ParameterAlignment: 9 | EnforcedStyle: with_fixed_indentation 10 | 11 | Layout/FirstArgumentIndentation: 12 | EnforcedStyle: consistent 13 | 14 | Layout/HeredocIndentation: 15 | EnforcedStyle: squiggly 16 | 17 | Layout/MultilineMethodCallBraceLayout: 18 | Enabled: false 19 | 20 | Layout/MultilineMethodCallIndentation: 21 | EnforcedStyle: indented 22 | 23 | # We use the symbols `:true` and `:false` in S-expressions. 24 | Lint/BooleanSymbol: 25 | Enabled: false 26 | 27 | # Not useful compared to e.g. AbcSize 28 | Metrics/BlockLength: 29 | Enabled: false 30 | 31 | # Not useful compared to e.g. AbcSize 32 | Metrics/ClassLength: 33 | Enabled: false 34 | 35 | # Not useful compared to e.g. AbcSize 36 | Metrics/MethodLength: 37 | Enabled: false 38 | 39 | # Not useful compared to e.g. AbcSize 40 | Metrics/ModuleLength: 41 | Enabled: false 42 | 43 | Metrics/ParameterLists: 44 | Max: 6 45 | 46 | # EOS is a well-known delimiter and thus satisfactory. 47 | Naming/HeredocDelimiterNaming: 48 | Enabled: false 49 | 50 | # Prefer semantic delimiters, but avoid `end.x`. Too subtle to lint. 51 | Style/BlockDelimiters: 52 | Enabled: false 53 | 54 | Style/Documentation: 55 | Exclude: 56 | - "spec/**/*" 57 | 58 | # Too subtle to lint. Avoid postfix operators unless line is very simple. 59 | Style/IfUnlessModifier: 60 | Enabled: false 61 | 62 | # Guard clauses are great, but this is too subtle to lint. 63 | Style/GuardClause: 64 | Enabled: false 65 | 66 | # Too subtle to lint. Avoid postfix operators unless line is very simple. 67 | Style/WhileUntilModifier: 68 | Enabled: false 69 | -------------------------------------------------------------------------------- /spec/lib/input/subprocessors/defn_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'ruby_parser' 5 | require 'minitest_to_rspec/input/model/defn' 6 | 7 | module MinitestToRspec 8 | module Input 9 | module Subprocessors 10 | RSpec.describe Defn do 11 | def parse(exp) 12 | RubyParser.new.parse(exp) 13 | end 14 | 15 | describe '.new' do 16 | context 'sexp_type is not :defn' do 17 | it 'raises TypeError' do 18 | expect { described_class.new(s(:str, 'derp'), false, false) } 19 | .to raise_error(TypeError) 20 | end 21 | end 22 | end 23 | 24 | describe '#example_title' do 25 | it 'parses the method name' do 26 | sexp = s(:defn, :test_method_name) 27 | expect(described_class.new(sexp, false, false).send(:example_title)) 28 | .to eq('method name') 29 | end 30 | end 31 | 32 | describe '#example_block' do 33 | it 'returns a sexp block and inner logic' do 34 | ruby = <<-RUBY 35 | def test_method_name 36 | assert_equal 1, 1 37 | end 38 | RUBY 39 | 40 | allow_any_instance_of(Model::Defn) 41 | .to receive(:body).and_return(s(parse('assert_equal 1, 1'))) 42 | sexp = RubyParser.new.parse(ruby) 43 | example_block = described_class 44 | .new(sexp, false, false) 45 | .send(:example_block) 46 | expect(example_block.first).to eq(:block) 47 | expect(example_block).to include(parse('expect(1).to(eq(1))')) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/subprocessors/defn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/processor' 4 | require 'minitest_to_rspec/input/subprocessors/base' 5 | require 'minitest_to_rspec/input/model/defn' 6 | 7 | module MinitestToRspec 8 | module Input 9 | module Subprocessors 10 | # Minitest tests can be defined as methods using names beginning with 11 | # 'test_'. Process those tests into RSpec `it` example blocks. 12 | class Defn < Base 13 | def initialize(sexp, rails, mocha) 14 | super(rails, mocha) 15 | @original = sexp.dup 16 | @exp = Model::Defn.new(sexp) 17 | sexp.clear 18 | end 19 | 20 | # Using a `Model::Defn`, returns a `Sexp` 21 | def process 22 | if @exp.test_method? 23 | s(:iter, 24 | s(:call, nil, :it, s(:str, example_title)), 25 | 0, 26 | example_block) 27 | elsif @exp.setup? 28 | s(:iter, 29 | s(:call, nil, :before), 30 | 0, 31 | example_block 32 | ) 33 | elsif @exp.teardown? 34 | s(:iter, 35 | s(:call, nil, :after), 36 | 0, 37 | example_block 38 | ) 39 | else 40 | @original 41 | end 42 | end 43 | 44 | private 45 | 46 | # Remove 'test_' prefix and replace underscores with spaces 47 | def example_title 48 | @exp.method_name.sub(/^test_/, '').tr('_', ' ') 49 | end 50 | 51 | def example_block 52 | block = s(:block) 53 | @exp.body.each_with_object(block) do |line, blk| 54 | blk << process_line(line) 55 | end 56 | end 57 | 58 | def process_line(line) 59 | Processor.new(@rails, @mocha).process(line) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-10-29 12:35:01 -0400 using RuboCop version 0.76.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 6 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: EnforcedStyle, IndentationWidth. 12 | # SupportedStyles: with_first_argument, with_fixed_indentation 13 | Layout/ArgumentAlignment: 14 | Exclude: 15 | - "spec/lib/input/subprocessors/klass_spec.rb" 16 | 17 | # Offense count: 13 18 | # Cop supports --auto-correct. 19 | Layout/ClosingParenthesisIndentation: 20 | Exclude: 21 | - "lib/minitest_to_rspec/input/subprocessors/base.rb" 22 | - "lib/minitest_to_rspec/input/subprocessors/call.rb" 23 | - "lib/minitest_to_rspec/input/subprocessors/defn.rb" 24 | - "lib/minitest_to_rspec/input/subprocessors/iter.rb" 25 | - "spec/lib/input/subprocessors/base_spec.rb" 26 | - "spec/lib/input/subprocessors/call_spec.rb" 27 | - "spec/lib/input/subprocessors/klass_spec.rb" 28 | 29 | # Offense count: 5 30 | # Cop supports --auto-correct. 31 | Layout/EmptyLineAfterGuardClause: 32 | Exclude: 33 | - "lib/minitest_to_rspec/cli.rb" 34 | - "lib/minitest_to_rspec/input/model/call.rb" 35 | - "lib/minitest_to_rspec/input/model/klass.rb" 36 | - "lib/minitest_to_rspec/input/subprocessors/call.rb" 37 | 38 | # Offense count: 2 39 | # Configuration parameters: EnforcedStyleForLeadingUnderscores. 40 | # SupportedStylesForLeadingUnderscores: disallowed, required, optional 41 | Naming/MemoizedInstanceVariableName: 42 | Exclude: 43 | - "lib/minitest_to_rspec/input/model/klass.rb" 44 | 45 | # Offense count: 2 46 | # Cop supports --auto-correct. 47 | Style/ExpandPathArguments: 48 | Exclude: 49 | - "minitest_to_rspec.gemspec" 50 | - "spec/spec_helper.rb" 51 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/rspec/stub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/type' 4 | 5 | module MinitestToRspec 6 | module Rspec 7 | # Represents a `receive` matcher from RSpec. 8 | # Conceptually the same as `Minitest::Stub`. 9 | class Stub 10 | def initialize(receiver, any_instance, message, with, returns, count) 11 | Type.assert(Sexp, receiver) 12 | Type.bool(any_instance) 13 | Type.assert(Sexp, message) 14 | Type.assert([NilClass, Sexp], with) 15 | Type.assert([NilClass, Sexp], returns) 16 | Type.assert([NilClass, Integer], count) 17 | @receiver = receiver 18 | @any_instance = any_instance 19 | @message = message 20 | @with = with 21 | @returns = returns 22 | @count = count 23 | end 24 | 25 | # Returns a Sexp representing an RSpec stub (allow) or message 26 | # expectation (expect) 27 | def to_rspec_exp 28 | stub_chain = s(:call, nil, :receive, @message) 29 | unless @with.nil? 30 | stub_chain = s(:call, stub_chain, :with, @with) 31 | end 32 | unless @returns.nil? 33 | stub_chain = s(:call, stub_chain, :and_return, @returns) 34 | end 35 | unless @count.nil? 36 | stub_chain = s(:call, stub_chain, receive_count_method) 37 | end 38 | expect_allow = s(:call, nil, rspec_mocks_method, @receiver.dup) 39 | s(:call, expect_allow, :to, stub_chain) 40 | end 41 | 42 | private 43 | 44 | def receive_count_method 45 | case @count 46 | when 1 47 | :once 48 | when 2 49 | :twice 50 | else 51 | raise "Unsupported message receive count: #{@count}" 52 | end 53 | end 54 | 55 | # Returns :expect or :allow 56 | def rspec_mocks_method 57 | prefix = @count.nil? ? 'allow' : 'expect' 58 | suffix = @any_instance ? '_any_instance_of' : '' 59 | (prefix + suffix).to_sym 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/iter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/model/base' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Data object. Represents an `:iter` s-expression. 9 | class Iter < Base 10 | def initialize(exp) 11 | assert_sexp_type(:iter, exp) 12 | @exp = exp.dup 13 | end 14 | 15 | def [](*args) 16 | @exp[*args] 17 | end 18 | 19 | def assert_difference? 20 | !empty? && Model::Call.assert_difference?(@exp[1]) 21 | end 22 | 23 | def assert_no_difference? 24 | !empty? && Model::Call.assert_no_difference?(@exp[1]) 25 | end 26 | 27 | def assert_nothing_raised? 28 | !empty? && Model::Call.assert_nothing_raised?(@exp[1]) 29 | end 30 | 31 | def assert_raise? 32 | !empty? && Model::Call.assert_raise?(@exp[1]) 33 | end 34 | 35 | def assert_raises? 36 | !empty? && Model::Call.assert_raises?(@exp[1]) 37 | end 38 | 39 | def refute_raise? 40 | !empty? && Model::Call.refute_raise?(@exp[1]) 41 | end 42 | 43 | def refute_raises? 44 | !empty? && Model::Call.refute_raises?(@exp[1]) 45 | end 46 | 47 | def block 48 | @exp[3] 49 | end 50 | 51 | def call 52 | @exp[1] 53 | end 54 | 55 | # Not to be confused with block arguments. 56 | def call_arguments 57 | call_obj.arguments 58 | end 59 | 60 | def call_obj 61 | Model::Call.new(call) 62 | end 63 | 64 | # Enumerates children, skipping the base `call` and 65 | # starting with the block arguments, then each `:call` in 66 | # the block. 67 | def each 68 | @exp[2..-1].each do |e| yield(e) end 69 | end 70 | 71 | def empty? 72 | @exp.length == 1 # just the sexp_type 73 | end 74 | 75 | def setup? 76 | !empty? && Model::Call.method_name?(@exp[1], :setup) 77 | end 78 | 79 | def teardown? 80 | !empty? && Model::Call.method_name?(@exp[1], :teardown) 81 | end 82 | 83 | def sexp 84 | @exp 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/minitest/stub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/errors' 4 | require 'minitest_to_rspec/type' 5 | require 'minitest_to_rspec/input/model/call' 6 | 7 | module MinitestToRspec 8 | module Minitest 9 | # Represents an `expects` or a `stubs` from mocha. 10 | # Conceptually the same as `Rspec::Stub`. 11 | class Stub 12 | def initialize(call) 13 | Type.assert(Input::Model::Call, call) 14 | @call = call 15 | end 16 | 17 | # Given e.g. `X.any_instance.expects(:y)`, returns `X`. 18 | def receiver 19 | chain = @call.receiver_chain 20 | last = chain[-1] 21 | last.nil? ? chain[-2] : last 22 | end 23 | 24 | # Returns true if we are stubbing any instance of `receiver`. 25 | def any_instance? 26 | @call.calls_in_receiver_chain.any? { |i| 27 | i.method_name.to_s.include?('any_instance') 28 | } 29 | end 30 | 31 | # Given e.g. `expects(:y)`, returns `:y`. 32 | def message 33 | case @call.method_name 34 | when :expects, :stubs 35 | @call.arguments.first 36 | else 37 | call = the_call_to_stubs_or_expects 38 | if call.nil? 39 | raise UnknownVariant, 'not a mocha stub, no stubs/expects' 40 | else 41 | call.arguments.first 42 | end 43 | end 44 | end 45 | 46 | def with 47 | @call.find_call_in_receiver_chain(:with)&.arguments&.first 48 | end 49 | 50 | def returns 51 | case @call.method_name 52 | when :returns 53 | @call.arguments.first 54 | else 55 | @call.find_call_in_receiver_chain(:returns)&.arguments&.first 56 | end 57 | end 58 | 59 | # TODO: add support for 60 | # - at_least 61 | # - at_least_once 62 | # - at_most 63 | # - at_most_once 64 | # - never 65 | def count 66 | case @call.method_name 67 | when :expects, :once 68 | 1 69 | when :returns 70 | the_call_to_stubs_or_expects.method_name == :expects ? 1 : nil 71 | when :twice 72 | 2 73 | end 74 | end 75 | 76 | private 77 | 78 | # Given an `exp` representing a chain of calls, like 79 | # `stubs(x).returns(y).once`, finds the call to `stubs` or `expects`. 80 | def the_call_to_stubs_or_expects 81 | @call.find_call_in_receiver_chain(%i[stubs expects]) 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/lib/converter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module MinitestToRspec 6 | RSpec.describe Converter do 7 | # Fixtures that represent rails use cases. These will 8 | # instantiate Converter with `rails: true`. 9 | RAILS_FIXTURES = [15, 20, 23].freeze 10 | 11 | # The path to `spec/fixtures` 12 | SPEC_FIXTURES = File.join(__dir__, '..', 'fixtures') 13 | 14 | # Directories under `spec/fixtures` 15 | FIXTURE_DIRS = Dir.glob("#{SPEC_FIXTURES}/*") 16 | 17 | def convert(input, file_path, rails, mocha) 18 | described_class.new(rails: rails, mocha: mocha).convert(input, file_path) 19 | end 20 | 21 | def input_file_path(fixture) 22 | File.join(fixture, 'in.rb') 23 | end 24 | 25 | def fixture_number(fixture) 26 | File.basename(fixture).split('_').first.to_i 27 | end 28 | 29 | def output_file_path(fixture) 30 | File.join(fixture, 'out.rb') 31 | end 32 | 33 | def rails?(fixture) 34 | RAILS_FIXTURES.include?(fixture_number(fixture)) 35 | end 36 | 37 | def read_input(fixture) 38 | File.read(input_file_path(fixture)) 39 | end 40 | 41 | def read_output(fixture) 42 | File.read(output_file_path(fixture)) 43 | end 44 | 45 | describe '#convert' do 46 | # To run just one of the following programmatically 47 | # generated examples, use RSpec's `--example` flag, e.g. 48 | # `rspec --example "17" spec/lib/converter_spec.rb` 49 | FIXTURE_DIRS.each do |fixture| 50 | it "converts: #{fixture}" do 51 | expected = read_output(fixture).strip 52 | input = read_input(fixture) 53 | path = input_file_path(fixture) 54 | calculated = convert(input, path, rails?(fixture), true).strip 55 | expect(calculated).to eq(expected) 56 | end 57 | end 58 | 59 | it 'supports rails option' do 60 | expect( 61 | convert("require 'test_helper'", nil, true, false) 62 | ).to eq('require("rails_helper")') 63 | end 64 | 65 | context '__FILE__ keyword' do 66 | it 'replaces with the given file path' do 67 | expect( 68 | convert('__FILE__', '/banana/kiwi/mango', false, false) 69 | ).to eq('"/banana/kiwi/mango"') 70 | end 71 | 72 | it 'replaces with helpful message when not provided' do 73 | expect( 74 | convert('__FILE__', nil, false, false) 75 | ).to match(/No file path provided/) 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'minitest_to_rspec' 5 | require 'optimist' 6 | 7 | module MinitestToRspec 8 | # Command-Line Interface (CLI) instantiated by `bin/mt2rspec` 9 | class CLI 10 | E_USAGE = 1 11 | E_FILE_NOT_FOUND = 2 12 | E_FILE_ALREADY_EXISTS = 3 13 | E_CONVERT_FAIL = 4 14 | E_CANNOT_CREATE_TARGET_DIR = 5 15 | 16 | BANNER = <<~EOS 17 | Usage: mt2rspec [--rails] [--mocha] source_file [target_file] 18 | 19 | Reads source_file, writes target_file. If target_file is omitted, 20 | its location will be inferred. For example, test/fruit/banana_test.rb 21 | implies spec/fruit/banana_spec.rb. If the target directory doesn't 22 | exist, it will be created. 23 | 24 | Options: 25 | EOS 26 | OPT_MOCHA = 'Convert mocha to rspec-mocks. (Experimental)' 27 | OPT_RAILS = <<~EOS.tr("\n", ' ').freeze 28 | Requires rails_helper instead of spec_helper. 29 | Passes :type metadatum to RSpec.describe. 30 | EOS 31 | 32 | attr_reader :source, :target 33 | 34 | def initialize(args) 35 | opts = parse_args(args) 36 | @rails = opts[:rails] 37 | @mocha = opts[:mocha] 38 | case args.length 39 | when 2 40 | @source, @target = args 41 | when 1 42 | @source = args[0] 43 | @target = infer_target_from @source 44 | else 45 | warn 'Please specify source file' 46 | exit E_USAGE 47 | end 48 | end 49 | 50 | def run 51 | assert_file_exists(source) 52 | assert_file_does_not_exist(target) 53 | ensure_target_directory(target) 54 | write_target(converter.convert(read_source, source)) 55 | rescue Error => e 56 | warn "Failed to convert: #{e}" 57 | exit E_CONVERT_FAIL 58 | end 59 | 60 | private 61 | 62 | def assert_file_exists(file) 63 | unless File.exist?(file) 64 | warn "File not found: #{file}" 65 | exit(E_FILE_NOT_FOUND) 66 | end 67 | end 68 | 69 | def assert_file_does_not_exist(file) 70 | if File.exist?(file) 71 | warn "File already exists: #{file}" 72 | exit(E_FILE_ALREADY_EXISTS) 73 | end 74 | end 75 | 76 | def converter 77 | Converter.new(mocha: @mocha, rails: @rails) 78 | end 79 | 80 | def ensure_target_directory(target) 81 | dir = File.dirname(target) 82 | return if Dir.exist?(dir) 83 | begin 84 | FileUtils.mkdir_p(dir) 85 | rescue SystemCallError => e 86 | warn "Cannot create target dir: #{dir}" 87 | warn e.message 88 | exit E_CANNOT_CREATE_TARGET_DIR 89 | end 90 | end 91 | 92 | def infer_target_from(source) 93 | source 94 | .gsub(/\Atest/, 'spec') 95 | .gsub(/_test.rb\Z/, '_spec.rb') 96 | end 97 | 98 | def parse_args(args) 99 | Optimist.options(args) do 100 | version MinitestToRspec.gem_version.to_s 101 | banner BANNER 102 | opt :rails, OPT_RAILS, short: :none 103 | opt :mocha, OPT_MOCHA, short: :none 104 | end 105 | end 106 | 107 | def read_source 108 | File.read(source) 109 | end 110 | 111 | def write_target(str) 112 | File.write(target, str) 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/subprocessors/klass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/errors' 4 | require 'minitest_to_rspec/input/model/klass' 5 | require 'minitest_to_rspec/input/subprocessors/base' 6 | 7 | module MinitestToRspec 8 | module Input 9 | module Subprocessors 10 | # Processes `s(:class, ..)` expressions. 11 | class Klass < Base 12 | # Takes `sexp`, a `:class` s-expression, and `rails`, a 13 | # boolean indicating that `rspec-rails` conventions (like 14 | # `:type` metadata) should be used. 15 | def initialize(sexp, rails, mocha) 16 | super(rails, mocha) 17 | @exp = Model::Klass.new(sexp) 18 | sexp.clear 19 | end 20 | 21 | def process 22 | sexp = head 23 | ebk = @exp.block 24 | if ebk.length > 1 25 | sexp << block 26 | elsif ebk.length == 1 27 | sexp << full_process(ebk[0]) 28 | end 29 | sexp 30 | end 31 | 32 | private 33 | 34 | # Returns a :block S-expression, the contents of the class. 35 | def block 36 | processed = @exp.block.map { |line| full_process(line) } 37 | s(:block, *processed) 38 | end 39 | 40 | # Given a `test_class_name` like `BananaTest`, returns the 41 | # described class, like `Banana`. 42 | def described_class(test_class_name) 43 | test_class_name.to_s.gsub(/Test\Z/, '').to_sym 44 | end 45 | 46 | # Returns the head of the output Sexp. If it's a test case, 47 | # an :iter representing an `RSpec.describe`. Otherwise, a :class. 48 | def head 49 | if @exp.test_case? 50 | rspec_describe_block 51 | else 52 | s(:class, @exp.name, @exp.parent) 53 | end 54 | end 55 | 56 | # Returns an S-expression representing the 57 | # RDM (RSpec Describe Metadata) hash 58 | def rdm 59 | s(:hash, s(:lit, :type), s(:lit, rdm_type)) 60 | end 61 | 62 | # Returns the RDM (RSpec Describe Metadata) type. 63 | # 64 | # > Model specs: type: :model 65 | # > Controller specs: type: :controller 66 | # > Request specs: type: :request 67 | # > Feature specs: type: :feature 68 | # > View specs: type: :view 69 | # > Helper specs: type: :helper 70 | # > Mailer specs: type: :mailer 71 | # > Routing specs: type: :routing 72 | # > http://bit.ly/1G5w7CJ 73 | # 74 | # TODO: Obviously, they're not all supported yet. 75 | def rdm_type 76 | if @exp.action_controller_test_case? 77 | :controller 78 | elsif @exp.draper_test_case? 79 | :decorator 80 | elsif @exp.action_mailer_test_case? 81 | :mailer 82 | else 83 | :model 84 | end 85 | end 86 | 87 | def rspec_describe 88 | const = s(:const, described_class(@exp.name)) 89 | call = s(:call, s(:const, :RSpec), :describe, const) 90 | call << rdm if @rails 91 | call 92 | end 93 | 94 | # Returns a S-expression representing a call to RSpec.describe 95 | def rspec_describe_block 96 | s(:iter, rspec_describe, 0) 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/klass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/model/base' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Data object. Represents a `:class` S-expression. 9 | class Klass < Base 10 | def initialize(exp) 11 | assert_sexp_type(:class, exp) 12 | @exp = exp.dup 13 | assert_valid_name 14 | end 15 | 16 | def action_controller_test_case? 17 | lineage?(parent, %i[ActionController TestCase]) 18 | end 19 | 20 | def action_mailer_test_case? 21 | lineage?(parent, %i[ActionMailer TestCase]) 22 | end 23 | 24 | def active_support_test_case? 25 | lineage?(parent, %i[ActiveSupport TestCase]) 26 | end 27 | 28 | # Raise an error if we don't know now to process the name 29 | # of this class. Specifically, classes with module-shorthand. 30 | def assert_valid_name 31 | if name.is_a?(Symbol) 32 | # Valid 33 | elsif name.respond_to?(:sexp_type) && name.sexp_type == :colon2 34 | raise ModuleShorthandError 35 | else 36 | raise ProcessingError, "Unexpected class expression: #{name}" 37 | end 38 | end 39 | 40 | def block? 41 | !block.empty? 42 | end 43 | 44 | def block 45 | @_block ||= @exp[3..-1] || [] 46 | end 47 | 48 | def test_unit_test_case? 49 | lineage?(parent, %i[Test Unit TestCase]) 50 | end 51 | 52 | def draper_test_case? 53 | lineage?(parent, %i[Draper TestCase]) 54 | end 55 | 56 | # Returns the name of the class. Examples: 57 | # 58 | # - Banana #=> :Banana 59 | # - Fruit::Banana #=> s(:colon2, s(:const, :Fruit), :Banana) 60 | # 61 | # Note that the latter (module shorthand) is not supported 62 | # by MinitestToRspec. See `#assert_valid_name`. 63 | # 64 | def name 65 | @exp[1] 66 | end 67 | 68 | # Returns the "inheritance". Examples: 69 | # 70 | # - Inherit nothing #=> nil 71 | # - Inherit Foo #=> s(:const, :Foo) 72 | # - Inherit Bar::Foo #=> s(:colon2, s(:const, :Bar), :Foo) 73 | # 74 | def parent 75 | @_parent ||= @exp[2] 76 | end 77 | 78 | # Returns true if `@exp` inherits from, e.g. ActiveSupport::TestCase. 79 | # TODO: Other test case parent classes. 80 | def test_case? 81 | return false unless sexp_type?(:colon2, parent) 82 | active_support_test_case? || 83 | action_controller_test_case? || 84 | action_mailer_test_case? || 85 | test_unit_test_case? || 86 | draper_test_case? 87 | end 88 | 89 | private 90 | 91 | def ancestor_names(exp) 92 | return [exp] if exp.is_a?(Symbol) 93 | 94 | sexp_type?(:colon2, exp) || sexp_type?(:const, exp) || 95 | raise(TypeError, "Expected :const or :colon2, got #{exp.inspect}") 96 | 97 | exp.sexp_body.flat_map { |entry| ancestor_names(entry) } 98 | end 99 | 100 | def lineage?(exp, names) 101 | assert_sexp_type(:colon2, exp) 102 | ancestor_names(exp) == names 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/subprocessors/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/sexp_assertions' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Subprocessors 8 | # Parent class of "sub-processors". There is one sub-processor for each 9 | # `sexp_type` that `Processor` knows how to process. 10 | # 11 | # For example, `Subprocessors::Call` will process an `s(:call, ..)` 12 | # expression representing minitest code, and return an S-expression 13 | # representing equivalent RSpec code. 14 | class Base 15 | include SexpAssertions 16 | 17 | def initialize(rails, mocha) 18 | @rails = rails 19 | @mocha = mocha 20 | end 21 | 22 | # Returns a s-expression representing an rspec-mocks stub. 23 | def allow_to(msg_recipient, matcher, any_instance = false) 24 | allow_method = any_instance ? :allow_any_instance_of : :allow 25 | target = s(:call, nil, allow_method, msg_recipient) 26 | s(:call, target, :to, matcher) 27 | end 28 | 29 | # Returns a s-expression representing an RSpec expectation, i.e. the 30 | # combination of an "expectation target" and a matcher. 31 | def expect(target, eager, phase, matcher, any_instance) 32 | et = expectation_target(target, eager, any_instance) 33 | s(:call, et, phase, matcher) 34 | end 35 | 36 | def expect_to(matcher, target, eager, any_instance = false) 37 | expect(target, eager, :to, matcher, any_instance) 38 | end 39 | 40 | def expect_to_not(matcher, target, eager) 41 | expect(target, eager, :to_not, matcher, false) 42 | end 43 | 44 | # In RSpec, `expect` returns an "expectation target". This 45 | # can be based on an expression, as in `expect(1 + 1)` or it 46 | # can be based on a block, as in `expect { raise }`. Either 47 | # way, it's called an "expectation target". 48 | def expectation_target(exp, eager, any_instance) 49 | if eager 50 | expectation_target_eager(exp, any_instance) 51 | else 52 | expectation_target_lazy(exp) 53 | end 54 | end 55 | 56 | def expectation_target_eager(exp, any_instance) 57 | expect_method = any_instance ? :expect_any_instance_of : :expect 58 | s(:call, nil, expect_method, exp) 59 | end 60 | 61 | def expectation_target_lazy(block) 62 | s(:iter, 63 | s(:call, nil, :expect), 64 | 0, 65 | full_process(block) 66 | ) 67 | end 68 | 69 | # If it's a `Sexp`, run `obj` through a new `Processor`. Otherwise, 70 | # return `obj`. 71 | # 72 | # This is useful for expressions that cannot be fully understood by a 73 | # single subprocessor. For example, we must begin processing all :iter 74 | # expressions, because some :iter represent calls we're interested in, 75 | # e.g. `assert_difference`. However, if the :iter turns out to be 76 | # uninteresting (perhaps it has no assertions) we still want to fully 77 | # process its sub-expressions. 78 | # 79 | # TODO: `full_process` may not be the best name. 80 | def full_process(obj) 81 | obj.is_a?(Sexp) ? Processor.new(@rails, @mocha).process(obj) : obj 82 | end 83 | 84 | def matcher(name, *args) 85 | exp = s(:call, nil, name) 86 | exp.concat(args) 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/lib/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'minitest_to_rspec/cli' 5 | 6 | module MinitestToRspec 7 | RSpec.describe CLI do 8 | let(:cli) { described_class.new([source, target]) } 9 | let(:source) { 'spec/fixtures/01_trivial_assertion/in.rb' } 10 | let(:target) { '/dev/null' } 11 | 12 | before(:all) do 13 | # Tests require certain files to exist 14 | # http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap10.html 15 | unless File.exist?('/dev/null') && Dir.exist?('/tmp') 16 | raise 'Tests require a POSIX OS' 17 | end 18 | end 19 | 20 | before(:each) do 21 | # Don't actually write target file 22 | allow(cli).to receive(:write_target) 23 | end 24 | 25 | describe '.new' do 26 | context 'target omitted' do 27 | it 'infers target' do 28 | cli = described_class.new(['test/fruit/banana_test.rb']) 29 | expect(cli.target).to eq('spec/fruit/banana_spec.rb') 30 | end 31 | end 32 | 33 | context 'no arguments' do 34 | it 'exits with status code E_USAGE' do 35 | # It also prints usage, but rspec `output` matcher only partially 36 | # works when combined with `raise_error(SystemExit)` matcher. 37 | expect { 38 | expect { described_class.new([]) }.to output.to_stderr 39 | }.to raise_error(SystemExit) { |e| 40 | expect(e.status).to eq(described_class::E_USAGE) 41 | } 42 | end 43 | end 44 | end 45 | 46 | describe '#run' do 47 | it 'converts source to target' do 48 | expect(cli).to receive(:assert_file_does_not_exist) 49 | cli.run 50 | end 51 | 52 | it 'catches MinitestToRspec::Error' do 53 | allow(cli).to receive(:assert_file_does_not_exist) 54 | allow(cli).to receive(:converter).and_raise(Error, 'so sad') 55 | expect { 56 | expect { cli.run }.to output('Failed to convert: so sad').to_stderr 57 | }.to raise_error(SystemExit) 58 | end 59 | end 60 | 61 | describe '#assert_file_does_not_exist' do 62 | context 'when file exists' do 63 | it 'exits with code E_FILE_ALREADY_EXISTS' do 64 | expect { 65 | expect { cli.run }.to output( 66 | 'File already exists: /dev/null' 67 | ).to_stderr 68 | }.to raise_error(SystemExit) 69 | end 70 | end 71 | end 72 | 73 | describe '#assert_file_exists' do 74 | context 'when file does not exist' do 75 | let(:source) { 'does_not_exist_3819e90182546cf5da27f193d0f3000164' } 76 | 77 | it 'exits with code E_FILE_NOT_FOUND' do 78 | expect { 79 | expect { cli.run }.to output( 80 | "File not found: #{source}" 81 | ).to_stderr 82 | }.to raise_error(SystemExit) 83 | end 84 | end 85 | end 86 | 87 | describe '#ensure_target_directory' do 88 | context 'when dir exists' do 89 | let(:target) { '/tmp/banana_spec.rb' } 90 | 91 | it 'returns nil and does not call mkdir_p' do 92 | expect(FileUtils).to_not receive(:mkdir_p) 93 | cli.run 94 | end 95 | end 96 | 97 | context 'when dir does not exist' do 98 | let(:target) { 99 | '/tmp/3819e90182546cf5da27f193d0f3000164/banana_spec.rb' 100 | } 101 | 102 | it 'calls mkdir_p' do 103 | expect(FileUtils).to receive(:mkdir_p).with( 104 | '/tmp/3819e90182546cf5da27f193d0f3000164' 105 | ) 106 | cli.run 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/subprocessors/iter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/subprocessors/base' 4 | require 'minitest_to_rspec/input/model/iter' 5 | 6 | module MinitestToRspec 7 | module Input 8 | module Subprocessors 9 | # Processes `s(:iter, ..)` expressions. 10 | class Iter < Base 11 | def initialize(sexp, rails, mocha) 12 | super(rails, mocha) 13 | @exp = Model::Iter.new(sexp) 14 | sexp.clear 15 | end 16 | 17 | def process 18 | process_exp(@exp) 19 | end 20 | 21 | private 22 | 23 | # Returns an expression representing an RSpec `change {}` 24 | # matcher. See also `change_by` below. 25 | def change(exp) 26 | matcher_with_block(:change, exp) 27 | end 28 | 29 | # Returns an expression representing an RSpec `change {}.by()` matcher. 30 | def change_by(diff_exp, by_exp) 31 | s(:call, 32 | change(diff_exp), 33 | :by, 34 | by_exp 35 | ) 36 | end 37 | 38 | def matcher_with_block(matcher_name, block) 39 | s(:iter, 40 | s(:call, nil, matcher_name), 41 | 0, 42 | block 43 | ) 44 | end 45 | 46 | def method_assert_difference(exp) 47 | call = exp[1] 48 | block = exp[3] 49 | by = call[4] 50 | what = parse(call[3][1]) 51 | matcher = by.nil? ? change(what) : change_by(what, by) 52 | expect_to(matcher, block, false) 53 | end 54 | 55 | def method_assert_no_difference(exp) 56 | call = exp[1] 57 | block = exp[3] 58 | what = parse(call[3][1]) 59 | expect_to_not(change(what), block, false) 60 | end 61 | 62 | def method_assert_nothing_raised(exp) 63 | block = exp[3] 64 | expect_to_not(raise_error, block, false) 65 | end 66 | 67 | def method_assert_raise(iter) 68 | method_assert_raises(iter) 69 | end 70 | 71 | def method_assert_raises(iter) 72 | expect_to(raise_error(*iter.call_arguments), iter.block, false) 73 | end 74 | 75 | def method_refute_raise(iter) 76 | method_refute_raises(iter) 77 | end 78 | 79 | def method_refute_raises(iter) 80 | expect_to_not(raise_error(*iter.call_arguments), iter.block, false) 81 | end 82 | 83 | def method_setup(exp) 84 | replace_method_name(exp, :before) 85 | end 86 | 87 | def method_teardown(exp) 88 | replace_method_name(exp, :after) 89 | end 90 | 91 | def name_of_processing_method(iter) 92 | method_name = iter[1][2] 93 | "method_#{method_name}".to_sym 94 | end 95 | 96 | def parse(str) 97 | RubyParser.new.parse(str) 98 | end 99 | 100 | # Given a `Model::Iter`, returns a `Sexp` 101 | def process_exp(exp) 102 | if processable?(exp) 103 | send_to_processing_method(exp) 104 | else 105 | process_uninteresting_iter(exp.sexp) 106 | end 107 | end 108 | 109 | def processable?(iter) 110 | if !iter.empty? && iter[1].sexp_type == :call 111 | method_name = iter[1][2] 112 | decision = "#{method_name}?".to_sym 113 | iter.respond_to?(decision) && iter.public_send(decision) 114 | else 115 | false 116 | end 117 | end 118 | 119 | def process_uninteresting_iter(exp) 120 | iter = s(exp.shift) 121 | until exp.empty? 122 | iter << full_process(exp.shift) 123 | end 124 | iter 125 | end 126 | 127 | # Given `args` which came from an `assert_raise` or an 128 | # `assert_raises`, return a `raise_error` matcher. 129 | # When the last argument is a string, it represents the 130 | # assertion failure message, and is discarded. 131 | def raise_error(*args) 132 | args.pop if !args.empty? && args.last.sexp_type == :str 133 | matcher(:raise_error, *args) 134 | end 135 | 136 | def replace_method_name(exp, new_method) 137 | iter = s(:iter, s(:call, nil, new_method)) 138 | exp.each do |e| iter << full_process(e) end 139 | iter 140 | end 141 | 142 | def send_to_processing_method(exp) 143 | send(name_of_processing_method(exp), exp) 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/lib/input/subprocessors/iter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'ruby_parser' 5 | 6 | module MinitestToRspec 7 | module Input 8 | module Subprocessors 9 | RSpec.describe Iter do 10 | describe '.new' do 11 | context 'not an :iter' do 12 | it 'raises error' do 13 | expect { 14 | described_class.new(s(:nil), false, false) 15 | }.to raise_error(TypeError) 16 | end 17 | end 18 | end 19 | 20 | describe '#process' do 21 | def parse(ruby) 22 | RubyParser.new.parse(ruby) 23 | end 24 | 25 | def process(input) 26 | described_class.new(input, true, true).process 27 | end 28 | 29 | it 'replaces assert_difference with expect to change' do 30 | input = parse <<-EOS 31 | assert_difference "ary.length" do ary.push("banana") end 32 | EOS 33 | output = parse <<-EOS 34 | expect { ary.push("banana") }.to(change { ary.length }) 35 | EOS 36 | expect(process(input)).to eq(output) 37 | end 38 | 39 | it 'replaces assert_difference (arity 2) with expect to change by' do 40 | input = parse <<-EOS 41 | assert_difference "ary.length", +1 do 42 | ary.push("banana") 43 | end 44 | EOS 45 | output = parse <<-EOS 46 | expect { ary.push("banana") }.to(change { ary.length }.by(+1)) 47 | EOS 48 | expect(process(input)).to eq(output) 49 | end 50 | 51 | it 'replaces assert_no_difference with expect to_not change' do 52 | input = parse <<-EOS 53 | assert_no_difference "banana.flavor" do 54 | banana.peel 55 | end 56 | EOS 57 | output = parse <<-EOS 58 | expect { banana.peel }.to_not(change { banana.flavor }) 59 | EOS 60 | expect(process(input)).to eq(output) 61 | end 62 | 63 | context 'assert_raise' do 64 | it 'replaces assert_raise with expect to raise' do 65 | input = parse <<-EOS 66 | assert_raise { Kiwi.delicious! } 67 | EOS 68 | output = parse <<-EOS 69 | expect { Kiwi.delicious! }.to raise_error 70 | EOS 71 | expect(process(input)).to eq(output) 72 | end 73 | 74 | it 'replaces assert_raise(e) with expect to raise_error(e)' do 75 | input = parse <<-EOS 76 | assert_raise(NotDeliciousError) { Kiwi.delicious! } 77 | EOS 78 | output = parse <<-EOS 79 | expect { Kiwi.delicious! }.to raise_error(NotDeliciousError) 80 | EOS 81 | expect(process(input)).to eq(output) 82 | end 83 | 84 | it 'replaces assert_raise(str) with raise_error' do 85 | input = parse <<-EOS 86 | assert_raise("Fruit should not be hairy") { Kiwi.delicious! } 87 | EOS 88 | output = parse <<-EOS 89 | expect { Kiwi.delicious! }.to raise_error 90 | EOS 91 | expect(process(input)).to eq(output) 92 | end 93 | 94 | it 'does not replace assert_raise(e1, e2)' do 95 | input = lambda { 96 | parse( 97 | 'assert_raise(NotDelicious, NotYellow) { Kiwi.delicious! }' 98 | ) 99 | } 100 | expect(process(input.call)).to eq(input.call) 101 | end 102 | 103 | it 'replaces assert_raise(e, str) with raise_error(e)' do 104 | input = parse <<-EOS 105 | assert_raise(NotDelicious, "Fruit should not be hairy") { 106 | Kiwi.delicious! 107 | } 108 | EOS 109 | output = parse <<-EOS 110 | expect { Kiwi.delicious! }.to raise_error(NotDelicious) 111 | EOS 112 | expect(process(input)).to eq(output) 113 | end 114 | end 115 | 116 | it 'replaces assert_raises with expect to raise' do 117 | input = parse <<-EOS 118 | assert_raises(NotDeliciousError) { Kiwi.delicious! } 119 | EOS 120 | output = parse <<-EOS 121 | expect { Kiwi.delicious! }.to(raise_error(NotDeliciousError)) 122 | EOS 123 | expect(process(input)).to eq(output) 124 | end 125 | 126 | it 'replaces assert_nothing_raised with expect to_not raise' do 127 | input = parse <<-EOS 128 | assert_nothing_raised { Banana.delicious! } 129 | EOS 130 | output = parse <<-EOS 131 | expect { Banana.delicious! }.to_not(raise_error) 132 | EOS 133 | expect(process(input)).to eq(output) 134 | end 135 | 136 | it 'replaces setup with before' do 137 | expect( 138 | process(parse('setup { peel_bananas }')) 139 | ).to eq( 140 | parse('before { peel_bananas }') 141 | ) 142 | end 143 | 144 | it 'replaces teardown with after' do 145 | expect( 146 | process(parse('teardown { compost_the_banana_peels }')) 147 | ).to eq( 148 | parse('after { compost_the_banana_peels }') 149 | ) 150 | end 151 | end 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/model/call.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/input/model/base' 4 | 5 | module MinitestToRspec 6 | module Input 7 | module Model 8 | # Data object. Represents a `:call` s-expression. 9 | class Call < Base 10 | attr_reader :original 11 | 12 | def initialize(exp) 13 | assert_sexp_type(:call, exp) 14 | @exp = exp.dup 15 | @original = exp.dup 16 | end 17 | 18 | class << self 19 | def assert_difference?(exp) 20 | exp.sexp_type == :call && new(exp).assert_difference? 21 | end 22 | 23 | def assert_no_difference?(exp) 24 | exp.sexp_type == :call && new(exp).assert_no_difference? 25 | end 26 | 27 | def assert_nothing_raised?(exp) 28 | exp.sexp_type == :call && new(exp).assert_nothing_raised? 29 | end 30 | 31 | def assert_raise?(exp) 32 | exp.sexp_type == :call && new(exp).assert_raise? 33 | end 34 | 35 | def assert_raises?(exp) 36 | exp.sexp_type == :call && new(exp).assert_raises? 37 | end 38 | 39 | def refute_raise?(exp) 40 | exp.sexp_type == :call && new(exp).refute_raise? 41 | end 42 | 43 | def refute_raises?(exp) 44 | exp.sexp_type == :call && new(exp).refute_raises? 45 | end 46 | 47 | def method_name?(exp, name) 48 | exp.sexp_type == :call && new(exp).method_name.to_s == name.to_s 49 | end 50 | end 51 | 52 | def arguments 53 | @exp[3..-1] || [] 54 | end 55 | 56 | def argument_types 57 | arguments.map(&:sexp_type) 58 | end 59 | 60 | def assert_difference? 61 | return false unless method_name == :assert_difference 62 | [[:str], %i[str lit]].include?(argument_types) 63 | end 64 | 65 | def assert_no_difference? 66 | method_name == :assert_no_difference && 67 | arguments.length == 1 && 68 | arguments[0].sexp_type == :str 69 | end 70 | 71 | def assert_nothing_raised? 72 | method_name == :assert_nothing_raised && arguments.empty? 73 | end 74 | 75 | def assert_raise? 76 | method_name == :assert_raise && raise_error_args? 77 | end 78 | 79 | def assert_raises? 80 | method_name == :assert_raises && raise_error_args? 81 | end 82 | 83 | def refute_raise? 84 | method_name == :refute_raise && raise_error_args? 85 | end 86 | 87 | def refute_raises? 88 | method_name == :refute_raises && raise_error_args? 89 | end 90 | 91 | def calls_in_receiver_chain 92 | receiver_chain.each_with_object([]) do |e, a| 93 | next unless sexp_type?(:call, e) 94 | a << self.class.new(e) 95 | end 96 | end 97 | 98 | def find_call_in_receiver_chain(method_names) 99 | name_array = [method_names].flatten 100 | calls_in_receiver_chain.find { |i| 101 | name_array.include?(i.method_name) 102 | } 103 | end 104 | 105 | def method_name 106 | @exp[2] 107 | end 108 | 109 | def num_arguments 110 | arguments.length 111 | end 112 | 113 | def one_string_argument? 114 | arguments.length == 1 && string?(arguments[0]) 115 | end 116 | 117 | # Returns true if arguments can be processed into RSpec's `raise_error` 118 | # matcher. When the last argument is a string, it represents the 119 | # assertion failure message, which will be discarded later. 120 | def raise_error_args? 121 | arg_types = arguments.map(&:sexp_type) 122 | [[], [:str], [:const], %i[const str], [:colon2]].include?(arg_types) 123 | end 124 | 125 | def receiver 126 | @exp[1] 127 | end 128 | 129 | # Consider the following chain of method calls: 130 | # 131 | # @a.b.c 132 | # 133 | # whose S-expression is 134 | # 135 | # s(:call, s(:call, s(:call, nil, :a), :b), :c) 136 | # 137 | # the "receiver chain" is 138 | # 139 | # [ 140 | # s(:call, s(:call, nil, :a), :b), 141 | # s(:call, nil, :a), 142 | # nil 143 | # ] 144 | # 145 | # The order of the returned array matches the order in which 146 | # messages are received, i.e. the order of execution. 147 | # 148 | # Note that the final receiver `nil` is included. This `nil` 149 | # represents the implicit receiver, e.g. `self` or `main`. 150 | # 151 | def receiver_chain 152 | receivers = [] 153 | ptr = @exp 154 | while sexp_type?(:call, ptr) 155 | receivers << ptr[1] 156 | ptr = ptr[1] 157 | end 158 | receivers 159 | end 160 | 161 | def receiver_chain_include?(method_name) 162 | receiver_chain.compact.any? { |r| 163 | self.class.method_name?(r, method_name) 164 | } 165 | end 166 | 167 | def require_test_helper? 168 | method_name == :require && 169 | one_string_argument? && 170 | arguments[0][1] == 'test_helper' 171 | end 172 | 173 | def question_mark_method? 174 | method_name.to_s.end_with?('?') 175 | end 176 | 177 | private 178 | 179 | def string?(exp) 180 | exp.sexp_type == :str 181 | end 182 | end 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project follows [semver 2.0.0][1] and the recommendations 4 | of [keepachangelog.com][2]. 5 | 6 | ## Unreleased 7 | 8 | ### Breaking Changes 9 | 10 | - None 11 | 12 | ### Added 13 | 14 | - None 15 | 16 | ### Fixed 17 | 18 | - None 19 | 20 | ## 0.13.0 (2018-10-18) 21 | 22 | ### Breaking Changes 23 | 24 | - Removed `MinitestToRspec::VERSION`, please use `MinitestToRspec.gem_version` 25 | 26 | ### Added 27 | 28 | - [#26](https://github.com/jaredbeck/minitest_to_rspec/pull/26) - 29 | Support `Test::Unit::TestCase` 30 | - [#25](https://github.com/jaredbeck/minitest_to_rspec/pull/25) - 31 | Support `assert_empty`, `assert_kind_of`, `assert_instance_of` 32 | 33 | ### Fixed 34 | 35 | - None 36 | 37 | ## 0.12.0 (2018-03-11) 38 | 39 | ### Breaking Changes 40 | 41 | - None 42 | 43 | ### Added 44 | 45 | - [#21](https://github.com/jaredbeck/minitest_to_rspec/pull/21) - 46 | convert `setup`/`teardown` methods to `before`/`after` blocks 47 | - [#20](https://github.com/jaredbeck/minitest_to_rspec/pull/20) - 48 | Support `refute_raise[s]` 49 | - [#18](https://github.com/jaredbeck/minitest_to_rspec/pull/18) - 50 | Support namespaced exceptions for assert_raise[s] 51 | 52 | ### Fixed 53 | 54 | - None 55 | 56 | ## 0.11.0 (2017-11-10) 57 | 58 | ### Breaking Changes 59 | 60 | - None 61 | 62 | ### Added 63 | 64 | - [#15](https://github.com/jaredbeck/minitest_to_rspec/pull/15) - 65 | Support mocha stubs without returns 66 | - [#16](https://github.com/jaredbeck/minitest_to_rspec/pull/16) - 67 | Convert `should` to `it` 68 | 69 | ### Fixed 70 | 71 | - None 72 | 73 | ## 0.10.2 (2017-11-09) 74 | 75 | ### Breaking Changes 76 | 77 | - None 78 | 79 | ### Added 80 | 81 | - None 82 | 83 | ### Fixed 84 | 85 | - [#12](https://github.com/jaredbeck/minitest_to_rspec/issues/12) - 86 | Only covert methods whose name begins with `test_` 87 | 88 | ## 0.10.1 (2017-11-04) 89 | 90 | ### Breaking Changes 91 | 92 | - None 93 | 94 | ### Added 95 | 96 | - None 97 | 98 | ### Fixed 99 | 100 | - [#10](https://github.com/jaredbeck/minitest_to_rspec/pull/10) - Lift 101 | version constraint on sexp_processor 102 | 103 | ## 0.10.0 (2017-11-01) 104 | 105 | ### Breaking Changes 106 | 107 | - Drop support for ruby < 2.3 so we can use frozen_string_literal 108 | - We may now return frozen strings, now that we are using frozen_string_literal. 109 | This could affect methods like `Converter#convert`. Even if some method seems 110 | to return a thawed string in some situations, users should simiply assume all 111 | strings are frozen. 112 | 113 | ### Added 114 | 115 | - MinitestToRspec.gem_version 116 | - [#7](https://github.com/jaredbeck/minitest_to_rspec/pull/7) - Support for 117 | converting methods named test_* 118 | 119 | ### Fixed 120 | 121 | - None 122 | 123 | ## 0.9.0 (2017-10-24) 124 | 125 | ### Breaking Changes 126 | 127 | - Drop support for ruby < 2.2 128 | 129 | ### Added 130 | 131 | None 132 | 133 | ### Fixed 134 | 135 | - [#4](https://github.com/jaredbeck/minitest_to_rspec/issues/4) - Constrain 136 | dependency: sexp_processor < 4.8 137 | 138 | ## 0.8.0 139 | 140 | ### Changed 141 | 142 | - No longer care about code style of output. See discussion in readme. 143 | - Drop support for ruby 2.0 144 | 145 | ### Added 146 | 147 | - Update ruby_parser to 3.8 (was 3.7) 148 | - Use the official ruby2ruby instead of my sketchy sexp2ruby fork 149 | 150 | ### Fixed 151 | 152 | None 153 | 154 | ## 0.7.1 155 | 156 | ### Changed 157 | 158 | None 159 | 160 | ### Added 161 | 162 | None 163 | 164 | ### Fixed 165 | 166 | - Update sexp2ruby to 0.0.4 (was 0.0.3) 167 | 168 | ## 0.7.0 169 | 170 | ### Changed 171 | - `assert` on a question-mark method converts to `eq(true)` instead 172 | of `be_truthy`. Likewise, `refute` converts to `eq(false)`. This is not 173 | guaranteed to be the same as minitest's fuzzy `assert`, but the convention of 174 | question-mark methods returning real booleans is strong. 175 | 176 | ### Added 177 | - Converts assert_not_nil 178 | - CLI 179 | - Added `--mocha` flag. If present, converts mocha to 180 | rspec-mocks. (Experimental) 181 | - Creates `target_file` directory if it does not exist 182 | - Experimental 183 | - mocha: with 184 | 185 | ### Fixed 186 | - `__FILE__` keyword in input 187 | 188 | ## 0.6.2 189 | 190 | ### Fixed 191 | - Make runtime dependency on trollop explicit: declare in gemspec 192 | - Improve output: Fewer unnecessary parentheses: to, to_not 193 | 194 | ## 0.6.1 195 | 196 | ### Fixed 197 | - Improve output: Fewer unnecessary parentheses 198 | 199 | ## 0.6.0 200 | 201 | ### Added 202 | - Converts 203 | - Draper::TestCase 204 | - ActionMailer::TestCase 205 | - Experimental 206 | - mocha: once, twice 207 | - Switch from ruby2ruby to sexp2ruby 208 | - Will have better output, e.g. ruby 1.9.3 hash syntax 209 | - Upgrade to ruby_parser 3.7 210 | 211 | ## 0.5.0 212 | 213 | ### Changed 214 | - Executable 215 | - Renamed from `minitest_to_rspec` to `mt2rspec` 216 | - The `target_file` argument is now optional 217 | 218 | ## 0.4.0 219 | 220 | ### Added 221 | - Experimental 222 | - Conversion of mocha to rspec-mocks 223 | - expects 224 | - any_instance 225 | 226 | ### Fixed 227 | - NoMethodError when input contains stabby lambda 228 | 229 | ## 0.3.0 230 | 231 | ### Added 232 | - Converts 233 | - `setup` and `teardown` to `before` and `after` 234 | - `assert_raise`, `assert_raises` 235 | - CLI option: `--rails` 236 | - Prints `rails_helper` instead of `spec_helper` 237 | - Adds `:type` metadata, eg. `:type => :controller` 238 | - So far, only supports `:model` and `:controller` 239 | - Experimental 240 | - Limited conversion of mocha to rspec-mocks 241 | - returns 242 | - stub 243 | - stub_everything 244 | - stubs 245 | - Ruby 1.9 hash syntax (See [ruby2ruby PR 37][3]) 246 | 247 | ### Fixed 248 | - Improved error message for class definition using module shorthand 249 | 250 | ## 0.2.1 251 | 252 | ### Fixed 253 | - Declare ruby2ruby as a runtime dependency 254 | 255 | ## 0.2.0 256 | 257 | ### Added 258 | - CLI. Usage: `minitest_to_rspec source_file target_file` 259 | 260 | ## 0.1.0 261 | 262 | Initial release. 11 assertions are supported. 263 | 264 | [1]: http://semver.org/spec/v2.0.0.html 265 | [2]: http://keepachangelog.com/ 266 | [3]: https://github.com/seattlerb/ruby2ruby/pull/37 267 | -------------------------------------------------------------------------------- /spec/lib/input/subprocessors/klass_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'ruby_parser' 5 | 6 | module MinitestToRspec 7 | module Input 8 | module Subprocessors 9 | RSpec.describe Klass do 10 | describe '#process' do 11 | def parse(str) 12 | RubyParser.new.parse(str) 13 | end 14 | 15 | def process(exp, rails = false, mocha = true) 16 | described_class.new(exp, rails, mocha).process 17 | end 18 | 19 | context 'unexpected class expression' do 20 | it 'raises ProcessingError' do 21 | expect { 22 | process(s(:class, 'Derp', nil)) 23 | }.to raise_error(ProcessingError) 24 | end 25 | end 26 | 27 | context 'input class is not test case' do 28 | it 'does not convert empty class' do 29 | input = -> { s(:class, :Derp, nil) } 30 | expect(process(input.call)).to eq(input.call) 31 | end 32 | 33 | it 'does not convert trivial class' do 34 | input = -> { s(:class, :Derp, nil, s(:call, nil, :puts)) } 35 | expect(process(input.call)).to eq(input.call) 36 | end 37 | end 38 | 39 | context 'ActiveSupport::TestCase' do 40 | context 'emtpy' do 41 | it 'converts' do 42 | input = s(:class, 43 | :BananaTest, 44 | s(:colon2, s(:const, :ActiveSupport), :TestCase) 45 | ) 46 | iter = process(input) 47 | expect(iter.sexp_type).to eq(:iter) 48 | expect(iter.length).to eq(3) # type, call, args 49 | call = iter[1] 50 | expect(call).to eq(parse('RSpec.describe(Banana)')) 51 | end 52 | end 53 | 54 | context 'not empty' do 55 | it 'converts' do 56 | inp = s(:class, 57 | :BananaTest, 58 | s(:colon2, s(:const, :ActiveSupport), :TestCase), 59 | s(:iter, 60 | s(:call, nil, :test, s(:str, 'is delicious')), 61 | 0, 62 | s(:call, nil, :puts) 63 | ) 64 | ) 65 | expect(process(inp)).to eq(parse( 66 | <<-EOS 67 | RSpec.describe(Banana) do 68 | it "is delicious" do 69 | puts 70 | end 71 | end 72 | EOS 73 | )) 74 | end 75 | end 76 | end 77 | 78 | it 'converts a ActionMailer::TestCase' do 79 | input = parse( 80 | 'class BananaMailerTest < ActionMailer::TestCase; end' 81 | ) 82 | expect(process(input, true)).to eq(parse( 83 | 'RSpec.describe(BananaMailer, type: :mailer) do; end' 84 | )) 85 | end 86 | 87 | it 'converts a Test::Unit::TestCase' do 88 | input = parse('class BananaTest < Test::Unit::TestCase; end') 89 | expect(process(input, true)).to eq(parse( 90 | 'RSpec.describe(Banana, type: :model) do; end' 91 | )) 92 | end 93 | 94 | it 'converts a Draper::TestCase' do 95 | input = parse('class BananaDecoratorTest < Draper::TestCase; end') 96 | expect(process(input, true)).to eq(parse( 97 | 'RSpec.describe(BananaDecorator, type: :decorator) do; end' 98 | )) 99 | end 100 | 101 | it 'converts a class with more than just a single test' do 102 | expect(process(parse( 103 | <<-EOS 104 | class BananaTest < ActiveSupport::TestCase 105 | include Monkeys 106 | fend_off_the_monkeys 107 | peel_bananas 108 | test "is delicious" do 109 | assert Banana.new.delicious? 110 | end 111 | end 112 | EOS 113 | ))).to eq(parse( 114 | <<-EOS 115 | RSpec.describe(Banana) do 116 | include Monkeys 117 | fend_off_the_monkeys 118 | peel_bananas 119 | it "is delicious" do 120 | expect(Banana.new.delicious?).to eq(true) 121 | end 122 | end 123 | EOS 124 | )) 125 | end 126 | 127 | context 'class definition with module shorthand' do 128 | it 'raises ModuleShorthandError' do 129 | expect { 130 | process( 131 | s(:class, 132 | s(:colon2, s(:const, :Fruit), :BananaTest), 133 | nil 134 | ) 135 | ) 136 | }.to raise_error(ModuleShorthandError, 137 | /Please convert your class definition to use nested modules/ 138 | ) 139 | end 140 | end 141 | 142 | context 'class that inherits from ActionController::TestCase' do 143 | it 'converts to describe with :type => :controller' do 144 | inp = <<-EOS 145 | class BananasControllerTest < ActionController::TestCase 146 | end 147 | EOS 148 | expect(process(parse(inp), true)).to eq(parse( 149 | <<-EOS 150 | RSpec.describe(BananasController, :type => :controller) do 151 | end 152 | EOS 153 | )) 154 | end 155 | end 156 | 157 | context 'class that inherits from ActiveSupport::TestCase' do 158 | it 'converts to describe with :type => :model' do 159 | inp = <<-EOS 160 | class BananaTest < ActiveSupport::TestCase 161 | end 162 | EOS 163 | expect(process(parse(inp), true)).to eq(parse( 164 | <<-EOS 165 | RSpec.describe(Banana, :type => :model) do 166 | end 167 | EOS 168 | )) 169 | end 170 | end 171 | end 172 | end 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MinitestToRspec 2 | 3 | Converts [minitest][8] files to [rspec][9]. 4 | 5 | [![Build Status][1]][2] 6 | 7 | - Selected assertions from [Test::Unit][26], [minitest][8], 8 | and [ActiveSupport][27] are converted to [rspec-expectations][25]. 9 | - Selected methods from [mocha][28] are converted to [rspec-mocks][24]. 10 | (Experimental) 11 | - Selected methods from [shoulda-context][36] 12 | 13 | ## Example 14 | 15 | Input: 16 | 17 | ```ruby 18 | require 'test_helper' 19 | class ArrayTest < ActiveSupport::TestCase 20 | test "changes length" do 21 | ary = [] 22 | assert_difference "ary.length" do 23 | ary.push(:x) 24 | end 25 | end 26 | end 27 | ``` 28 | 29 | Output: 30 | 31 | ```ruby 32 | require("spec_helper") 33 | RSpec.describe(Array) do 34 | it("changes length") do 35 | ary = [] 36 | expect { ary.push(:x) }.to(change { ary.length }) 37 | end 38 | end 39 | ``` 40 | 41 | You might not like the code style of the output. More on that below. 42 | 43 | ## Install 44 | 45 | ```bash 46 | gem install minitest_to_rspec 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### CLI 52 | 53 | ```bash 54 | mt2rspec [--rails] [--mocha] source_file [target_file] 55 | mt2rspec --help 56 | ``` 57 | 58 | ### Ruby 59 | 60 | ```ruby 61 | require 'minitest_to_rspec' 62 | converter = MinitestToRspec::Converter.new(rails: false, mocha: false) 63 | converter.convert("assert('banana')") 64 | #=> "expect(\"banana\").to(be_truthy)" 65 | ``` 66 | 67 | ## Output 68 | 69 | The only goal is correctness. [Code style][34] is not a consideration. 70 | Providing the level of configuration necessary to make everyone happy would 71 | be a huge distraction from the main purpose. 72 | 73 | After conversion, I recommend using [rubocop][35]'s awesome `--auto-correct` 74 | feature to apply your preferred code style. 75 | 76 | Comments are discarded by [ruby_parser][14], so we have no way of 77 | preserving them. 78 | 79 | ## Supported Assertions 80 | 81 | Selected assertions from [minitest][8], [Test::Unit][26], and 82 | [ActiveSupport][27]. See [doc/supported_assertions.md][5] for rationale. 83 | Contributions are welcome. 84 | 85 | Assertion | Arity | Source 86 | --------------------------- | ----- | ------ 87 | assert | | 88 | assert_difference | 1,2 | 89 | [assert_equal][23] | 2,3 | Test::Unit 90 | [assert_not_equal][22] | 2,3 | Test::Unit 91 | assert_match | | 92 | assert_nil | | 93 | assert_not_nil | | 94 | [assert_no_difference][12] | | ActiveSupport 95 | [assert_nothing_raised][10] | | Test::Unit 96 | [assert_raise][11] | 0..2 | Test::Unit 97 | [assert_raises][13] | 0..2 | Minitest 98 | refute | | 99 | refute_equal | | 100 | [refute_raise][39] | | 101 | [refute_raises][39] | | 102 | 103 | ## Supported Mocha 104 | 105 | Mocha | Arity | Block | Notes 106 | --------------------- | ----- | ----- | ------- 107 | [any_instance][29] | 0 | n/a | 108 | [expects][21] | 1 | n/a | 109 | [once][31] | 0 | n/a | 110 | [stub][19] | 0,1,2 | no | 111 | [stub_everything][18] | 0,1,2 | no | Uses `as_null_object`, not the same. 112 | [stubs][20] | 1 | n/a | 113 | [twice][32] | 0 | n/a | 114 | 115 | To do: [at_least, never, raises, etc.][30] 116 | 117 | ## Supported shoulda-context methods 118 | 119 | Mocha | Arity | Block | Notes 120 | --------------------- | ----- | ----- | ------- 121 | [context][36] | 1 | yes | 122 | [setup][37] | 1,2 | no | 123 | [should][38] | 1,2 | yes | 124 | 125 | ## Acknowledgements 126 | 127 | This project would not be possible without [ruby_parser][14], 128 | [sexp_processor][15], and [ruby2ruby][16] by [Ryan Davis][17]. 129 | 130 | [1]: https://travis-ci.org/jaredbeck/minitest_to_rspec.svg?branch=master 131 | [2]: https://travis-ci.org/jaredbeck/minitest_to_rspec 132 | [5]: https://github.com/jaredbeck/minitest_to_rspec/blob/master/doc/supported_assertions.md 133 | [6]: https://github.com/seattlerb/ruby2ruby 134 | [8]: https://github.com/jaredbeck/minitest_to_rspec/blob/master/doc/minitest.md 135 | [9]: https://github.com/jaredbeck/minitest_to_rspec/blob/master/doc/rspec.md 136 | [10]: http://www.rubydoc.info/gems/test-unit/3.0.9/Test/Unit/Assertions#assert_nothing_raised-instance_method 137 | [11]: http://ruby-doc.org/stdlib-2.1.0/libdoc/test/unit/rdoc/Test/Unit/Assertions.html#method-i-assert_raise 138 | [12]: http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference 139 | [13]: http://www.rubydoc.info/gems/minitest/5.5.1/Minitest/Assertions#assert_raises-instance_method 140 | [14]: https://github.com/seattlerb/ruby_parser 141 | [15]: https://github.com/seattlerb/sexp_processor 142 | [16]: https://github.com/seattlerb/ruby2ruby 143 | [17]: https://github.com/zenspider 144 | [18]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/API:stub_everything 145 | [19]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/API#stub-instance_method 146 | [20]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/ObjectMethods#stubs-instance_method 147 | [21]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/ObjectMethods:expects 148 | [22]: http://www.rubydoc.info/gems/test-unit/3.0.9/Test/Unit/Assertions#assert_not_equal-instance_method 149 | [23]: http://www.rubydoc.info/gems/test-unit/3.0.9/Test/Unit/Assertions#assert_equal-instance_method 150 | [24]: https://github.com/rspec/rspec-mocks 151 | [25]: https://github.com/rspec/rspec-expectations 152 | [26]: http://test-unit.github.io/ 153 | [27]: https://rubygems.org/gems/activesupport 154 | [28]: http://gofreerange.com/mocha/docs/ 155 | [29]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/ClassMethods#any_instance-instance_method 156 | [30]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/Expectation 157 | [31]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/Expectation#once-instance_method 158 | [32]: http://www.rubydoc.info/github/floehopper/mocha/Mocha/Expectation#twice-instance_method 159 | [34]: https://github.com/bbatsov/ruby-style-guide 160 | [35]: https://github.com/bbatsov/rubocop 161 | [35]: https://github.com/thoughtbot/shoulda-context 162 | [36]: http://www.rubydoc.info/github/thoughtbot/shoulda-context/master/Shoulda/Context/ClassMethods#context-instance_method 163 | [37]: http://www.rubydoc.info/github/thoughtbot/shoulda-context/master/Shoulda/Context/Context#setup-instance_method 164 | [38]: http://www.rubydoc.info/github/thoughtbot/shoulda-context/master/Shoulda/Context/ClassMethods#should-instance_method 165 | [39]: https://github.com/jaredbeck/minitest_to_rspec/pull/20 166 | -------------------------------------------------------------------------------- /lib/minitest_to_rspec/input/subprocessors/call.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest_to_rspec/errors' 4 | require 'minitest_to_rspec/input/model/call' 5 | require 'minitest_to_rspec/input/model/hash_exp' 6 | require 'minitest_to_rspec/input/subprocessors/base' 7 | require 'minitest_to_rspec/minitest/stub' 8 | require 'minitest_to_rspec/rspec/stub' 9 | require 'minitest_to_rspec/type' 10 | 11 | module MinitestToRspec 12 | module Input 13 | module Subprocessors 14 | # Processes `s(:call, ..)` expressions. 15 | class Call < Base 16 | # Mocha methods will only be processed if `--mocha` flag was given, 17 | # i.e. `mocha` argument in constructor is true. 18 | MOCHA_METHODS = %i[ 19 | expects 20 | once 21 | returns 22 | stub 23 | stubs 24 | stub_everything 25 | twice 26 | ].freeze 27 | 28 | def initialize(sexp, rails, mocha) 29 | super(rails, mocha) 30 | @exp = Model::Call.new(sexp) 31 | sexp.clear 32 | end 33 | 34 | # Given a `Model::Call`, returns a `Sexp` 35 | def process 36 | if process? 37 | send(name_of_processing_method) 38 | else 39 | @exp.original 40 | end 41 | end 42 | 43 | def process? 44 | respond_to?(name_of_processing_method, true) && 45 | (@mocha || !MOCHA_METHODS.include?(@exp.method_name)) 46 | end 47 | 48 | private 49 | 50 | def be_falsey 51 | matcher(:be_falsey) 52 | end 53 | 54 | def be_nil 55 | matcher(:be_nil) 56 | end 57 | 58 | def be_empty 59 | matcher(:be_empty) 60 | end 61 | 62 | def be_truthy 63 | matcher(:be_truthy) 64 | end 65 | 66 | def be_a(exp) 67 | matcher(:be_a, exp) 68 | end 69 | 70 | def be_instance_of(exp) 71 | matcher(:be_instance_of, exp) 72 | end 73 | 74 | def call_to_question_mark?(exp) 75 | sexp_type?(:call, exp) && Model::Call.new(exp).question_mark_method? 76 | end 77 | 78 | def eq(exp) 79 | matcher(:eq, exp) 80 | end 81 | 82 | def match(pattern) 83 | matcher(:match, pattern) 84 | end 85 | 86 | def method_assert 87 | refsert eq(s(:true)), be_truthy 88 | end 89 | 90 | def method_assert_equal 91 | expected = @exp.arguments[0] 92 | calculated = @exp.arguments[1] 93 | expect_to(eq(expected), calculated, true) 94 | end 95 | 96 | def method_assert_match 97 | pattern = @exp.arguments[0] 98 | string = @exp.arguments[1] 99 | expect_to(match(pattern), string, true) 100 | end 101 | 102 | def method_assert_nil 103 | expect_to(be_nil, @exp.arguments[0], true) 104 | end 105 | 106 | def method_assert_not_nil 107 | expect_to_not(be_nil, @exp.arguments[0], true) 108 | end 109 | 110 | def method_assert_empty 111 | expect_to(be_empty, @exp.arguments[0], true) 112 | end 113 | 114 | def method_assert_not_equal 115 | expected = @exp.arguments[0] 116 | calculated = @exp.arguments[1] 117 | expect_to_not(eq(expected), calculated, true) 118 | end 119 | 120 | def method_assert_kind_of 121 | expected = @exp.arguments[0] 122 | calculated = @exp.arguments[1] 123 | expect_to(be_a(expected), calculated, true) 124 | end 125 | 126 | def method_assert_instance_of 127 | expected = @exp.arguments[0] 128 | calculated = @exp.arguments[1] 129 | expect_to(be_instance_of(expected), calculated, true) 130 | end 131 | 132 | def method_expects 133 | if @exp.num_arguments == 1 && 134 | %i[lit hash].include?(@exp.arguments.first.sexp_type) 135 | mocha_stub 136 | else 137 | @exp.original 138 | end 139 | end 140 | 141 | def method_once 142 | mocha_stub 143 | end 144 | 145 | def method_refute 146 | refsert eq(s(:false)), be_falsey 147 | end 148 | 149 | def method_refute_equal 150 | unexpected = @exp.arguments[0] 151 | calculated = @exp.arguments[1] 152 | expect_to_not(eq(unexpected), calculated, true) 153 | end 154 | 155 | # Processes an entire line of code that ends in `.returns` 156 | def method_returns 157 | if @exp.num_arguments.zero? 158 | @exp.original 159 | else 160 | mocha_stub 161 | end 162 | end 163 | 164 | def method_require 165 | if @exp.require_test_helper? 166 | require_spec_helper 167 | else 168 | @exp.original 169 | end 170 | end 171 | 172 | def method_should 173 | s(:call, nil, :it, *@exp.arguments) 174 | end 175 | 176 | # Happily, the no-block signatures of [stub][3] are the 177 | # same as [double][2]. 178 | # 179 | # - (name) 180 | # - (stubs) 181 | # - (name, stubs) 182 | def method_stub 183 | raise ArgumentError unless @exp.is_a?(Model::Call) 184 | if @exp.receiver.nil? 185 | s(:call, nil, :double, *@exp.arguments) 186 | else 187 | @exp.original 188 | end 189 | end 190 | 191 | # [stub_everything][1] responds to all messages with nil. 192 | # [double.as_null_object][4] responds with self. Not a 193 | # drop-in replacement, but will work in many situations. 194 | # RSpec doesn't provide an equivalent to `stub_everything`, 195 | # AFAIK. 196 | def method_stub_everything 197 | if @exp.receiver.nil? 198 | d = s(:call, nil, :double, *@exp.arguments) 199 | s(:call, d, :as_null_object) 200 | else 201 | @exp.original 202 | end 203 | end 204 | 205 | def method_test 206 | s(:call, nil, :it, *@exp.arguments) 207 | end 208 | 209 | def method_twice 210 | mocha_stub 211 | end 212 | 213 | # Given a sexp representing the hash from a mocha shorthand stub, as in 214 | # `Banana.expects(edible: true, color: "yellow")` 215 | # return an array of separate RSpec stubs, one for each hash key. 216 | def mocha_shorthand_stub_to_rspec_stubs(shorthand_stub_hash, mt_stub) 217 | Model::HashExp.new(shorthand_stub_hash).to_h.map { |k, v| 218 | Rspec::Stub.new( 219 | mt_stub.receiver, 220 | mt_stub.any_instance?, 221 | k, 222 | mt_stub.with, 223 | v, 224 | 1 225 | ).to_rspec_exp 226 | } 227 | end 228 | 229 | def mocha_stub 230 | mt_stub = Minitest::Stub.new(@exp) 231 | msg = mt_stub.message 232 | if sexp_type?(:hash, msg) 233 | pointless_lambda(mocha_shorthand_stub_to_rspec_stubs(msg, mt_stub)) 234 | else 235 | Rspec::Stub.new( 236 | mt_stub.receiver, 237 | mt_stub.any_instance?, 238 | mt_stub.message, 239 | mt_stub.with, 240 | mt_stub.returns, 241 | mt_stub.count 242 | ).to_rspec_exp 243 | end 244 | rescue UnknownVariant 245 | @exp.original 246 | end 247 | 248 | def name_of_processing_method 249 | "method_#{@exp.method_name}".to_sym 250 | end 251 | 252 | # Given `array_of_calls`, returns a `Sexp` representing a 253 | # self-executing lambda. 254 | # 255 | # This works around the fact that `sexp_processor` expects us to return 256 | # a single `Sexp`, not an array of `Sexp`. We also can't return a 257 | # `:block`, or else certain input would produce nested blocks (e.g. 258 | # `s(:block, s(:block, ..))`) which `ruby2ruby` (naturally) does not 259 | # know how to process. So, the easiest solution I could think of is a 260 | # self-executing lambda. 261 | # 262 | # Currently, the only `:call` which we process into multiple calls is 263 | # the hash form of a mocha `#expects`, thankfully uncommon. 264 | # 265 | # To get better output (without a pointless lambda) we would have to 266 | # process `:block` *and* `:defn`, which we are not yet doing. 267 | def pointless_lambda(array_of_calls) 268 | assert_sexp_type_array(:call, array_of_calls) 269 | s(:call, 270 | s(:iter, 271 | s(:call, nil, :lambda), 272 | 0, 273 | s(:block, 274 | s(:str, 'Sorry for the pointless lambda here.'), 275 | *array_of_calls 276 | ) 277 | ), 278 | :call 279 | ) 280 | end 281 | 282 | # `refsert` - Code shared by refute and assert. I could also have gone 283 | # with `assfute`. Wooo .. time for bed. 284 | def refsert(exact, fuzzy) 285 | actual = @exp.arguments[0] 286 | matcher = call_to_question_mark?(actual) ? exact : fuzzy 287 | expect_to(matcher, actual, true) 288 | end 289 | 290 | def require_spec_helper 291 | prefix = @rails ? 'rails' : 'spec' 292 | s(:call, nil, :require, s(:str, "#{prefix}_helper")) 293 | end 294 | 295 | # Wraps `obj` in an `Array` if it is a `Sexp` 296 | def wrap_sexp(obj) 297 | obj.is_a?(Sexp) ? [obj] : obj 298 | end 299 | end 300 | end 301 | end 302 | end 303 | 304 | # [1]: http://bit.ly/1yll6ND 305 | # [2]: http://bit.ly/1CRdmP3 306 | # [3]: http://bit.ly/1aY2mJN 307 | # [4]: http://bit.ly/1OtwDOY 308 | -------------------------------------------------------------------------------- /spec/lib/input/subprocessors/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'ruby_parser' 5 | 6 | module MinitestToRspec 7 | module Input 8 | module Subprocessors 9 | RSpec.describe Call do 10 | # Returns an S-expression representing a method call. 11 | def exp(method_name, argument) 12 | s(:call, nil, method_name.to_sym, s(:str, argument.to_s)) 13 | end 14 | 15 | def parse(ruby) 16 | RubyParser.new.parse(ruby) 17 | end 18 | 19 | def process(input, rails = false, mocha = true) 20 | described_class.new(input, rails, mocha).process 21 | end 22 | 23 | context 'assert' do 24 | it 'replaces `assert` with `expect` to be_truthy' do 25 | expect( 26 | process(parse('assert Banana')) 27 | ).to eq( 28 | parse('expect(Banana).to be_truthy') 29 | ) 30 | end 31 | 32 | it 'replaces question-mark `assert` with `expect` to eq(true)' do 33 | expect( 34 | process(parse('assert Banana.delicious?')) 35 | ).to eq( 36 | parse('expect(Banana.delicious?).to eq(true)') 37 | ) 38 | end 39 | end 40 | 41 | context 'assert_equal' do 42 | it 'replaces assert_equal (two args) with expect to eq' do 43 | expect( 44 | process(parse('assert_equal false, Kiwi.new.delicious?')) 45 | ).to eq( 46 | parse('expect(Kiwi.new.delicious?).to eq(false)') 47 | ) 48 | end 49 | 50 | it 'replaces assert_equal (three args) with expect to eq' do 51 | expect( 52 | process(parse("assert_equal false, Kiwi.new.delicious?, 'asdf'")) 53 | ).to eq( 54 | parse('expect(Kiwi.new.delicious?).to eq(false)') 55 | ) 56 | end 57 | end 58 | 59 | context 'assert_match' do 60 | it 'replaces assert_match with expect to match' do 61 | expect( 62 | process(parse('assert_match(/nana\Z/, "banana")')) 63 | ).to eq( 64 | parse('expect("banana").to match(/nana\Z/)') 65 | ) 66 | end 67 | end 68 | 69 | context 'assert_nil' do 70 | it 'replaces assert_nil with expect to be_nil' do 71 | expect( 72 | process(parse('assert_nil(kiwi)')) 73 | ).to eq( 74 | parse('expect(kiwi).to(be_nil)') 75 | ) 76 | end 77 | end 78 | 79 | context 'assert_not_equal' do 80 | it 'replaces assert_not_equal (two args) with expect to_not eq' do 81 | expect( 82 | process(parse('assert_not_equal(:banana, :kiwi)')) 83 | ).to eq( 84 | parse('expect(:kiwi).to_not eq(:banana)') 85 | ) 86 | end 87 | 88 | it 'replaces assert_not_equal (three args) with expect to_not eq' do 89 | expect( 90 | process(parse("assert_not_equal(:banana, :kiwi, 'asdf')")) 91 | ).to eq( 92 | parse('expect(:kiwi).to_not eq(:banana)') 93 | ) 94 | end 95 | end 96 | 97 | context 'create' do 98 | it 'does not change factory call' do 99 | input = lambda { 100 | s(:call, 101 | nil, 102 | :create, 103 | s(:lit, :banana), 104 | s(:hash, 105 | s(:lit, :peel), 106 | s(:call, nil, :peel), 107 | s(:lit, :color), 108 | s(:str, 'yellow'), 109 | s(:lit, :delicious), 110 | s(:true) 111 | ) 112 | ) 113 | } 114 | expect(process(input.call)).to eq(input.call) 115 | end 116 | end 117 | 118 | context 'expects' do 119 | it 'converts single expects to receive' do 120 | expect( 121 | process(parse('Banana.expects(:delicious?).returns(true)')) 122 | ).to eq( 123 | parse( 124 | 'expect(Banana).to receive(:delicious?).and_return(true).once' 125 | ) 126 | ) 127 | end 128 | 129 | it 'converts single expects with call receiver, to receive' do 130 | expect( 131 | process(parse('a_b.expects(:c?).returns(:d)')) 132 | ).to eq( 133 | parse('expect(a_b).to receive(:c?).and_return(:d).once') 134 | ) 135 | end 136 | 137 | it 'converts hash-expects to many receives' do 138 | expect(process(parse( 139 | 'Banana.expects(edible: true, color: "yellow")' 140 | ))).to eq(parse( 141 | <<-EOS 142 | lambda { 143 | "Sorry for the pointless lambda here." 144 | expect(Banana).to(receive(:edible).and_return(true).once) 145 | expect(Banana).to(receive(:color).and_return("yellow").once) 146 | }.call 147 | EOS 148 | )) 149 | end 150 | 151 | it 'converts expects without return' do 152 | expect( 153 | process(parse('Banana.expects(:delicious?)')) 154 | ).to eq( 155 | parse('expect(Banana).to receive(:delicious?).once') 156 | ) 157 | end 158 | 159 | context 'variants which should not be converted' do 160 | it 'does not replace non-mocha returns' do 161 | input = -> { parse('tax.returns') } 162 | expect(process(input.call)).to eq(input.call) 163 | end 164 | 165 | it 'does not replace expects with arity > 1' do 166 | input = -> { parse('tax.expects(:arm, :leg)') } 167 | expect(process(input.call)).to eq(input.call) 168 | end 169 | 170 | it 'does not replace expects with unknown argument type' do 171 | input = -> { parse('foo.expects(a_call_exp)') } 172 | expect(process(input.call)).to eq(input.call) 173 | end 174 | end 175 | end 176 | 177 | context 'once' do 178 | it 'replaces once with expect to receive once' do 179 | expect( 180 | process(parse('a.expects(:b).once')) 181 | ).to eq( 182 | parse('expect(a).to receive(:b).once') 183 | ) 184 | end 185 | end 186 | 187 | context 'refute' do 188 | it 'replaces `refute` with `expect` to be_falsey' do 189 | expect( 190 | process(parse('refute Kiwi')) 191 | ).to eq( 192 | parse('expect(Kiwi).to be_falsey') 193 | ) 194 | end 195 | 196 | it 'replaces question-mark `refute` with `expect` to be falsey' do 197 | expect( 198 | process(parse('refute Kiwi.delicious?')) 199 | ).to eq( 200 | parse('expect(Kiwi.delicious?).to eq(false)') 201 | ) 202 | end 203 | end 204 | 205 | context 'refute_equal' do 206 | it 'replaces refute_equal with expect to_not eq' do 207 | expect( 208 | process(parse('refute_equal(true, Kiwi.new.delicious?)')) 209 | ).to eq( 210 | parse('expect(Kiwi.new.delicious?).to_not eq(true)') 211 | ) 212 | end 213 | end 214 | 215 | context 'returns' do 216 | it 'replaces stubs/returns with expect to receive' do 217 | expect( 218 | process(parse('Banana.stubs(:delicious?).returns(true)')) 219 | ).to eq( 220 | parse('allow(Banana).to receive(:delicious?).and_return(true)') 221 | ) 222 | end 223 | 224 | it 'converts any_instance.expects' do 225 | expect( 226 | process( 227 | parse('Banana.any_instance.stubs(:delicious?).returns(true)') 228 | ) 229 | ).to eq( 230 | parse( 231 | 'allow_any_instance_of(Banana).to ' \ 232 | 'receive(:delicious?).and_return(true)' 233 | ) 234 | ) 235 | end 236 | 237 | it 'converts any_instance.stubs' do 238 | expect( 239 | process( 240 | parse('Banana.any_instance.expects(:delicious?).returns(true)') 241 | ) 242 | ).to eq( 243 | parse( 244 | 'expect_any_instance_of(Banana).to ' \ 245 | 'receive(:delicious?).and_return(true).once' 246 | ) 247 | ) 248 | end 249 | 250 | it "does not replace every method named 'returns'" do 251 | # In this example, `returns` does not represent a mocha stub. 252 | input = -> { parse('Banana.returns(peel)') } 253 | expect(process(input.call)).to eq(input.call) 254 | end 255 | end 256 | 257 | context 'require' do 258 | it 'does not replace unknown requires' do 259 | input = -> { parse("require 'a_shrubbery'") } 260 | expect(process(input.call)).to eq(input.call) 261 | end 262 | 263 | context 'rails option is false' do 264 | it 'replaces test_helper with spec_helper' do 265 | input = exp(:require, 'test_helper') 266 | expect(process(input, false)).to eq(exp(:require, 'spec_helper')) 267 | end 268 | end 269 | 270 | context 'rails option is true' do 271 | it 'replaces test_helper with rails_helper' do 272 | input = exp(:require, 'test_helper') 273 | expect(process(input, true)).to eq(exp(:require, 'rails_helper')) 274 | end 275 | end 276 | end 277 | 278 | context 'stub' do 279 | it 'replaces stub with double' do 280 | expect(process(parse('stub'))).to eq(parse('double')) 281 | end 282 | 283 | it 'replaces stub(hash) with double' do 284 | expect( 285 | process(parse('stub(:delicious? => true)')) 286 | ).to eq( 287 | parse('double(:delicious? => true)') 288 | ) 289 | end 290 | 291 | it 'does not replace explicit receiver stub' do 292 | input = -> { parse('pencil.stub') } 293 | expect(process(input.call)).to eq(input.call) 294 | end 295 | end 296 | 297 | context 'stub_everything' do 298 | it 'replaces with double as_null_object' do 299 | expect( 300 | process(parse('stub_everything')) 301 | ).to eq( 302 | parse('double.as_null_object') 303 | ) 304 | end 305 | 306 | context 'with explicit receiver' do 307 | it 'does not replace' do 308 | input = -> { parse('pencil.stub_everything') } 309 | expect(process(input.call)).to eq(input.call) 310 | end 311 | end 312 | 313 | context 'with specific allowed methods' do 314 | it 'replaces with double as_null_object' do 315 | expect( 316 | process(parse('stub_everything(:delicious? => false)')) 317 | ).to eq( 318 | parse('double(:delicious? => false).as_null_object') 319 | ) 320 | end 321 | end 322 | end 323 | 324 | context 'should' do 325 | it 'replaces `should` with `it`' do 326 | argument = 'is delicious' 327 | input = exp(:should, argument) 328 | expect(process(input)).to eq(exp(:it, argument)) 329 | end 330 | end 331 | 332 | context 'test' do 333 | it 'replaces `test` with `it`' do 334 | argument = 'is delicious' 335 | input = exp(:test, argument) 336 | expect(process(input)).to eq(exp(:it, argument)) 337 | end 338 | end 339 | 340 | context 'twice' do 341 | it 'replaces twice with expect to receive twice' do 342 | expect( 343 | process(parse('a.expects(:b).twice')) 344 | ).to eq( 345 | parse('expect(a).to receive(:b).twice') 346 | ) 347 | end 348 | end 349 | end 350 | end 351 | end 352 | end 353 | --------------------------------------------------------------------------------