Gem Version Dependency Status Build Status Coverage Status Code Climate

Transpec

Transpec automatically converts your specs into latest RSpec syntax with static analysis.

This aims to facilitate smooth transition to RSpec 3.

See the following pages for the new RSpec syntax and the plan for RSpec 3:

Note that Transpec does not yet support all conversions for the RSpec changes, and also the changes for RSpec 3 is not fixed and may vary in the future. So it's recommended to follow updates of both RSpec and Transpec.

Example

Here's an example spec:

describe Account do
  subject(:account) { Account.new(logger) }
  let(:logger) { mock('logger') }

  describe '#balance' do
    context 'initially' do
      it 'is zero' do
        .balance.should == 0
      end
    end
  end

  describe '#close' do
    it 'logs an account closed message' do
      logger.should_receive(:account_closed).with()
      .close
    end
  end

  describe '#renew' do
    context 'when the account is renewable and not closed' do
      before do
        .stub(:renewable? => true, :closed? => false)
      end

      it 'does not raise error' do
        lambda { .renew }.should_not raise_error(Account::RenewalError)
      end
    end
  end
end

Transpec would convert it to the following form:

describe Account do
  subject(:account) { Account.new(logger) }
  let(:logger) { double('logger') }

  describe '#balance' do
    context 'initially' do
      it 'is zero' do
        expect(.balance).to eq(0)
      end
    end
  end

  describe '#close' do
    it 'logs an account closed message' do
      expect(logger).to receive(:account_closed).with()
      .close
    end
  end

  describe '#renew' do
    context 'when the account is renewable and not closed' do
      before do
        allow().to receive(:renewable?).and_return(true)
        allow().to receive(:closed?).and_return(false)
      end

      it 'does not raise error' do
        expect { .renew }.not_to raise_error
      end
    end
  end
end

Installation

$ gem install transpec

Basic Usage

Before converting your specs:

  • Make sure your project has rspec gem dependency 2.14 or later. If not, change your *.gemspec or Gemfile to do so.
  • Run rspec and check if all the specs pass.
  • Ensure the Git repository is clean. (You don't want to mix up your changes and Transpec's changes, right?)

Then, run transpec with no arguments in the project root directory:

$ cd some-project
$ transpec
Processing spec/spec_helper.rb
Processing spec/spec_spec.rb
Processing spec/support/file_helper.rb
Processing spec/support/shared_context.rb
Processing spec/transpec/ast/scanner_spec.rb
Processing spec/transpec/ast/scope_stack_spec.rb

This will convert and overwrite all spec files in the spec directory.

After the conversion, run rspec again and check if all pass.

Options

-f/--force

Force processing even if the current Git repository is not clean.

$ git status --short
 M spec/spec_helper.rb
$ transpec
The current Git repository is not clean. Aborting.
$ transpec --force
Processing spec/spec_helper.rb
Processing spec/spec_spec.rb
Processing spec/support/file_helper.rb

-d/--disable

Disable specific conversions.

$ transpec --disable expect_to_receive,allow_to_receive

Available conversion types

Conversion Type Target Syntax Converted Syntax
expect_to_matcher obj.should matcher expect(obj).to matcher
expect_to_receive obj.should_receive expect(obj).to receive
allow_to_receive obj.stub allow(obj).to receive
deprecated obj.stub!, mock('foo'), etc. obj.stub, double('foo')

-n/--negative-form

Specify negative form of to that is used in expect syntax. Either not_to or to_not. not_to is used by default.

$ transpec --negative-form to_not

-p/--no-parentheses-matcher-arg

Suppress parenthesizing argument of matcher when converting should with operator matcher to expect with non-operator matcher (expect syntax does not directly support the operator matchers). Note that it will be parenthesized even if this option is specified when parentheses are necessary to keep the meaning of the expression.

describe 'original spec' do
  it 'is an example' do
    1.should == 1
    2.should > 1
    'string'.should =~ /^str/
    [1, 2, 3].should =~ [2, 1, 3]
    { key: value }.should == { key: value }
  end
end

describe 'converted spec' do
  it 'is an example' do
    expect(1).to eq(1)
    expect(2).to be > 1
    expect('string').to match(/^str/)
    expect([1, 2, 3]).to match_array([2, 1, 3])
    expect({ key: value }).to eq({ key: value })
  end
end

describe 'converted spec with -p/--no-parentheses-matcher-arg option' do
  it 'is an example' do
    expect(1).to eq 1
    expect(2).to be > 1
    expect('string').to match /^str/
    expect([1, 2, 3]).to match_array [2, 1, 3]
    # With non-operator method, the parentheses are always required
    # to prevent the hash from being interpreted as a block.
    expect({ key: value }).to eq({ key: value })
  end
end

Supported Conversions

Standard expectations

# Target
obj.should matcher
obj.should_not matcher

# Converted
expect(obj).to matcher
expect(obj).not_to matcher
expect(obj).to_not matcher # with `--negative-form to_not`

Operator matchers

# Target
1.should == 1
1.should < 2
Integer.should === 1
'string'.should =~ /^str/
[1, 2, 3].should =~ [2, 1, 3]

# Converted
expect(1).to eq(1)
expect(1).to be < 2
expect(Integer).to be === 1
expect('string').to match(/^str/)
expect([1, 2, 3]).to match_array([2, 1, 3])

be_close matcher

# Target
(1.0 / 3.0).should be_close(0.333, 0.001)

# Converted
(1.0 / 3.0).should be_within(0.001).of(0.333)

Expectations on Proc

# Target
lambda { do_something }.should raise_error
proc { do_something }.should raise_error
-> { do_something }.should raise_error

# Converted
expect { do_something }.to raise_error

Negative error expectations with specific error

# Target
expect { do_something }.not_to raise_error(SomeErrorClass)
expect { do_something }.not_to raise_error('message')
expect { do_something }.not_to raise_error(SomeErrorClass, 'message')
lambda { do_something }.should_not raise_error(SomeErrorClass)

# Converted
expect { do_something }.not_to raise_error
lambda { do_something }.should_not raise_error # with `--disable expect_to_matcher`

Message expectations

# Target
obj.should_receive(:foo)
SomeClass.any_instance.should_receive(:foo)

# Converted
expect(obj).to receive(:foo)
expect_any_instance_of(SomeClass).to receive(:foo)

Message expectations that are actually method stubs

# Target
obj.should_receive(:foo).any_number_of_times
obj.should_receive(:foo).at_least(0)

SomeClass.any_instance.should_receive(:foo).any_number_of_times
SomeClass.any_instance.should_receive(:foo).at_least(0)

# Converted
allow(obj).to receive(:foo)
obj.stub(:foo) # with `--disable allow_to_receive`

allow_any_instance_of(SomeClass).to receive(:foo)
SomeClass.any_instance.stub(:foo) # with `--disable allow_to_receive`

Method stubs

# Target
obj.stub(:foo)

obj.stub!(:foo)

obj.stub(:foo => 1, :bar => 2)

SomeClass.any_instance.stub(:foo)

# Converted
allow(obj).to receive(:foo)

allow(obj).to receive(:foo)

allow(obj).to receive(:foo).and_return(1)
allow(obj).to receive(:bar).and_return(2)

allow_any_instance_of(SomeClass).to receive(:foo)

Deprecated method stub aliases

# Target
obj.stub!(:foo)
obj.unstub!(:foo)

# Converted
obj.stub(:foo) # with `--disable allow_to_receive`
obj.unstub(:foo)

Method stubs with deprecated specification of number of times

# Target
obj.stub(:foo).any_number_of_times
obj.stub(:foo).at_least(0)

# Converted
allow(obj).to receive(:foo)
obj.stub(:foo) # with `--disable allow_to_receive`

Deprecated test double aliases

# Target
stub('something')
mock('something')

# Converted
double('something')

Compatibility

Tested on MRI 1.9, MRI 2.0 and JRuby in 1.9 mode.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request