Transpec
Transpec is a tool for converting your specs to the latest RSpec syntax with static and dynamic code analysis.
This aims to facilitate smooth transition to RSpec 3, and it's now ready for RSpec 2.99 and 3.0 beta!
See the following pages for the new RSpec syntax and the plan for RSpec 3:
- Myron Marston » RSpec's New Expectation Syntax
- RSpec's new message expectation syntax - Tea is awesome.
- Myron Marston » The Plan for RSpec 3
Transpec now supports almost all conversions for the RSpec changes, but 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.
Examples
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
account.balance.should == 0
end
end
end
describe '#close' do
it 'logs an account closed message' do
logger.should_receive(:account_closed).with(account)
account.close
end
end
describe '#renew' do
context 'when the account is renewable and not closed' do
before do
account.stub(:renewable? => true, :closed? => false)
end
it 'does not raise error' do
lambda { account.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(account.balance).to eq(0)
end
end
end
describe '#close' do
it 'logs an account closed message' do
expect(logger).to receive(:account_closed).with(account)
account.close
end
end
describe '#renew' do
context 'when the account is renewable and not closed' do
before do
allow(account).to receive(:renewable?).and_return(true)
allow(account).to receive(:closed?).and_return(false)
end
it 'does not raise error' do
expect { account.renew }.not_to raise_error
end
end
end
end
Actual examples
You can see actual conversion examples below:
- https://github.com/yujinakayama/guard/commit/transpec-demo
- https://github.com/yujinakayama/mail/commit/transpec-demo
- https://github.com/yujinakayama/twitter/commit/transpec-demo
Installation
Simply install transpec with gem command:
$ gem install transpec
Usually you don't need to add transpec to your *.gemspec or Gemfile since this isn't a tool to be used daily.
Basic Usage
Before converting your specs:
- Make sure your project has
rspecgem dependency 2.14 or later. If not, change your*.gemspecorGemfileto do so. - Run
rspecand 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 in the project root directory:
$ cd some-project
$ transpec
Copying the project for dynamic analysis...
Running dynamic analysis with command "bundle exec rspec"...
...............................................................................
...................
Finished in 13.07 seconds
100 examples, 0 failures
Converting spec/spec_helper.rb
Converting spec/support/cache_helper.rb
Converting spec/support/file_helper.rb
Converting spec/support/shared_context.rb
Converting spec/transpec/ast/node_spec.rb
This will run the specs, convert them, and overwrite all spec files in the spec directory.
After the conversion, run rspec again and check whether everything is green:
$ bundle exec rspec
If it's green, commit the changes with an auto-generated message that describes the conversion summary:
$ git commit -aeF .git/COMMIT_EDITMSG
And you are done!
Upgrade Process to RSpec 3 beta
If you are going to use Transpec in the upgrade process to RSpec 3 beta, read the article by Myron Marston:
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
Copying project for dynamic analysis...
Running dynamic analysis with command "bundle exec rspec"...
-s/--skip-dynamic-analysis
Skip dynamic analysis and convert with only static analysis. Note that specifying this option decreases the conversion accuracy.
-c/--rspec-command
Specify a command to run your specs that is used for dynamic analysis.
Transpec needs to run your specs in a copied project directory for dynamic analysis.
If your project requires some special setups or commands to run specs, use this option.
bundle exec rspec is used by default.
$ transpec --rspec-command "./special_setup.sh && bundle exec rspec"
-k/--keep
Keep specific syntaxes by disabling conversions.
$ transpec --keep should_receive,stub
Available syntax types
| Type | Target Syntax | Converted Syntax |
|---|---|---|
should |
obj.should matcher |
expect(obj).to matcher |
oneliner |
it { should ... } |
it { is_expected.to ... } |
should_receive |
obj.should_receive |
expect(obj).to receive |
stub |
obj.stub |
allow(obj).to receive |
have_items |
expect(obj).to have(n).items |
expect(obj.size).to eq(n) |
its |
its(:attr) { } |
describe { subject { }; it { } } |
deprecated |
obj.stub!, mock('foo'), etc. |
obj.stub, double('foo') |
See Supported Conversions for more details.
-n/--negative-form
Specify a negative form of to that is used in the expect syntax.
Either not_to or to_not.
not_to is used by default.
$ transpec --negative-form to_not
-b/--boolean-matcher
Specify a matcher type that be_true and be_false will be converted to.
Any of truthy,falsey, truthy,falsy or true,false can be specified.
truthy,falsey is used by default.
$ transpec --boolean-matcher true,false
See Supported Conversions - Boolean matchers for more details.
-p/--no-parentheses-matcher-arg
Suppress parenthesizing arguments of matchers when converting
should with operator matcher to expect with non-operator matcher
(the 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
Inconvertible Specs
You might see the following warning while conversion:
Cannot convert #should into #expect since #expect is not available in the context.
spec/awesome_spec.rb:4: 1.should == 1
This message would be shown with specs like:
describe '#should that cannot be converted to #expect' do
class MyAwesomeTestRunner
def run
1.should == 1
end
end
it 'is 1' do
test_runner = MyAwesomeTestRunner.new
test_runner.run
end
end
Reason
shouldis defined onBasicObjectclass, so you can useshouldeverywhere.expectis defined onRSpec::Matchersmodule that is included byRSpec::Core::ExampleGroupclass, so you can useexpectonly whereselfis an instance ofRSpec::Core::ExampleGroup(i.e. initblocks,:eachhook blocks or included module methods) or other classes that explicitly includeRSpec::Matchers.
With the above example, in the context of 1.should == 1, the self is an instance of MyAwesomeTestRunner.
Transpec tracks contexts and skips conversion if the target syntax cannot be converted in a case like this.
Solution
Include or extend RSpec::Matchers module to make expect available in the context:
class MyAwesomeTestRunner
include RSpec::Matchers
def run
1.should == 1
end
end
Then run transpec again.
Supported Conversions
Standard expectations
# Targets
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`
- Conversion can be disabled by:
--keep should - Deprecation: Deprecated since RSpec 3.0
- See also: Myron Marston » RSpec's New Expectation Syntax
One-liner expectations
This conversion is available only if your project has rspec gem dependency 2.99.0.beta2 or later.
# Targets
it { should matcher }
it { should_not matcher }
# Converted
it { is_expected.to matcher }
it { is_expected.not_to matcher }
it { is_expected.to_not matcher } # with `--negative-form to_not`
is_expected.to is designed for the consistency with the expect syntax.
However the one-liner should is still not deprecated in RSpec 3.0
and available even if the should syntax is disabled with RSpec.configure.
So if you think is_expected.to is verbose,
feel free to disable this conversion and continue using the one-liner should.
- Conversion can be disabled by:
--keep oneliner - Deprecation: Not deprecated
- See also: Add
is_expectedfor expect-based one-liner syntax. by myronmarston · rspec/rspec-core
Operator matchers
# Targets
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])
This conversion is combined with the conversion of standard expectations and cannot be disabled separately because the expect syntax does not directly support the operator matchers.
Boolean matchers
This conversion is available only if your project has rspec gem dependency 2.99.0.beta1 or later.
# Targets
expect(obj).to be_true
expect(obj).to be_false
# Converted
expect(obj).to be_truthy
expect(obj).to be_falsey
# With `--boolean-matcher truthy,falsy`
# be_falsy is just an alias of be_falsey.
expect(obj).to be_truthy
expect(obj).to be_falsy
# With `--boolean-matcher true,false`
expect(obj).to be true
expect(obj).to be false
be_truematcher passes if expectation subject is truthy in conditional semantics. (i.e. all objects exceptfalseandnil)be_falsematcher passes if expectation subject is falsey in conditional semantics. (i.e.falseornil)be_truthyandbe_falseymatchers are renamed version ofbe_trueandbe_falseand their behaviors are same.be trueandbe falseare not new things. These are combinations ofbematcher and boolean literals. These pass if expectation subject is exactly equal to boolean value.
So, converting be_true/be_false to be_truthy/be_falsey never breaks your specs and this is the Transpec's default. If you are willing to test boolean values strictly, you can convert them to be true/be false with --boolean-matcher true,false option. Note that this may break your specs if your application codes don't return exact boolean values.
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.99, removed in RSpec 3.0
- See also: Consider renaming
be_trueandbe_falsetobe_truthyandbe_falsey· rspec/rspec-expectations
be_close matcher
# Targets
expect(1.0 / 3.0).to be_close(0.333, 0.001)
# Converted
expect(1.0 / 3.0).to be_within(0.001).of(0.333)
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.1, removed in RSpec 3.0
- See also: New be within matcher and RSpec.deprecate fix · rspec/rspec-expectations
have(n).items matcher
This conversion will be disabled automatically if rspec-collection_matchers or rspec-rails is loaded in your spec.
# Targets
expect(collection).to have(3).items
expect(collection).to have_exactly(3).items
expect(collection).to have_at_least(3).items
expect(collection).to have_at_most(3).items
collection.should have(3).items
# Assume `team` responds to #players.
expect(team).to have(3).players
# Assume #players is a private method.
expect(team).to have(3).players
# Converted
expect(collection.size).to eq(3)
expect(collection.size).to be >= 3
expect(collection.size).to be <= 3
collection.size.should == 3 # with `--keep should`
expect(team.players.size).to eq(3)
# have(n).items matcher invokes #players even if it's a private method.
expect(team.send(:players).size).to eq(3)
There's an option to continue using have(n).items matcher with rspec-collection_matchers that is a gem extracted from rspec-expectations.
If you choose so, disable this conversion by either:
- Specify
--keep have_itemsoption manually. - Require
rspec-collection_matchersorrspec-railsin your spec so that Transpec automatically disables this conversion.
Note: rspec-rails 3.0 still uses have(n).items matcher with rspec-collection_matchers.
- Conversion can be disabled by:
--keep have_items - Deprecation: Deprecated since RSpec 2.99, removed in RSpec 3.0
- See also: Expectations: have(x).items matchers will be moved into an external gem - The Plan for RSpec 3
One-liner expectations with have(n).items matcher
This conversion will be disabled automatically if rspec-collection_matchers or rspec-rails is loaded in your spec.
# Targets
it { should have(3).items }
it { should have_at_least(3).players }
# Converted
it 'has 3 items' do
expect(subject.size).to eq(3)
end
# With `--keep should`
it 'has 3 items' do
subject.size.should == 3
end
it 'has at least 3 players' do
expect(subject.players.size).to be >= 3
end
- Conversion can be disabled by:
--keep have_items
Expectations on block
# Targets
lambda { do_something }.should raise_error
proc { do_something }.should raise_error
-> { do_something }.should raise_error
# Converted
expect { do_something }.to raise_error
- Conversion can be disabled by:
--keep should - Deprecation: Deprecated since RSpec 3.0
- See also: Unification of Block vs. Value Syntaxes - RSpec's New Expectation Syntax
Negative error expectations with specific error
# Targets
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 `--keep should`
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.14, removed in RSpec 3.0
- See also: Consider deprecating
expect { }.not_to raise_error(SpecificErrorClass)· rspec/rspec-expectations
Message expectations
# Targets
obj.should_receive(:foo)
Klass.any_instance.should_receive(:foo)
# Converted
expect(obj).to receive(:foo)
expect_any_instance_of(Klass).to receive(:foo)
- Conversion can be disabled by:
--keep should_receive - Deprecation: Deprecated since RSpec 3.0
- See also: RSpec's new message expectation syntax - Tea is awesome.
Message expectations that are actually method stubs
# Targets
obj.should_receive(:foo).any_number_of_times
obj.should_receive(:foo).at_least(0)
Klass.any_instance.should_receive(:foo).any_number_of_times
Klass.any_instance.should_receive(:foo).at_least(0)
# Converted
allow(obj).to receive(:foo)
obj.stub(:foo) # with `--keep stub`
allow_any_instance_of(Klass).to receive(:foo)
Klass.any_instance.stub(:foo) # with `--keep stub`
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.14, removed in RSpec 3.0
- See also: Don't allow at_least(0) · rspec/rspec-mocks
Method stubs
# Targets
obj.stub(:foo)
obj.stub!(:foo)
obj.stub(:foo => 1, :bar => 2)
obj.stub_chain(:foo, :bar, :baz)
Klass.any_instance.stub(:foo)
# Converted
allow(obj).to receive(:foo)
# If the target project's rspec gem dependency is prior to 3.0.0.beta1
allow(obj).to receive(:foo).and_return(1)
allow(obj).to receive(:bar).and_return(2)
# If the target project's rspec gem dependency is 3.0.0.beta1 or later
allow(obj).to (:foo => 1, :bar => 2)
# Conversion from `stub_chain` to `receive_message_chain` is available
# only if the target project's rspec gem dependency is 3.0.0.beta2 or later
allow(obj).to (:foo, :bar, :baz)
allow_any_instance_of(Klass).to receive(:foo)
No replacement for unstub
There's no replacement for unstub in the expect syntax. See the discussion for more details.
Steps to upgrade obj.stub(:foo => 1, :bar => 2)
allow(obj).to receive_messages(:foo => 1, :bar => 2) that is designed to be the replacement for obj.stub(:foo => 1, :bar => 2) is available from RSpec 3.0 (though it's now being considered to be backported to RSpec 2.99). So, in the upgrade path to RSpec 3, if you want to convert them with keeping the syntax correspondence, you need to follow these steps:
- Upgrade to RSpec 2.99
- Run
transpec --keep stub - Upgrade to RSpec 3.0
- Run
transpec
Otherwise obj.stub(:foo => 1, :bar => 2) will be converted to two allow(obj).to receive(...).and_return(...) expressions on RSpec 2.99.
- Conversion can be disabled by:
--keep stub - Deprecation: Deprecated since RSpec 3.0
- See also:
Deprecated method stub aliases
# Targets
obj.stub!(:foo)
obj.unstub!(:foo)
# Converted
obj.stub(:foo) # with `--keep stub`
obj.unstub(:foo)
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.14, removed in RSpec 3.0
- See also: Consider deprecating and/or removing #stub! and #unstub! at some point · rspec/rspec-mocks
Method stubs with deprecated specification of number of times
# Targets
obj.stub(:foo).any_number_of_times
obj.stub(:foo).at_least(0)
# Converted
allow(obj).to receive(:foo)
obj.stub(:foo) # with `--keep stub`
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.14, removed in RSpec 3.0
- See also: Don't allow at_least(0) · rspec/rspec-mocks
Deprecated test double aliases
# Targets
stub('something')
mock('something')
# Converted
double('something')
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.14, removed in RSpec 3.0
- See also: myronmarston / why_double.md - Gist
Expectations on attribute of subject with its
This conversion will be disabled automatically if rspec-its is loaded in your spec.
# Targets
describe 'example' do
subject { { foo: 1, bar: 2 } }
its(:size) { should == 2 }
its([:foo]) { should == 1 }
its('keys.first') { should == :foo }
end
# Converted
describe 'example' do
subject { { foo: 1, bar: 2 } }
describe '#size' do
subject { super().size }
it { should == 2 }
end
describe '[:foo]' do
subject { super()[:foo] }
it { should == 1 }
end
describe '#keys' do
subject { super().keys }
describe '#first' do
subject { super().first }
it { should == :foo }
end
end
end
There's an option to continue using its with rspec-its that is a gem extracted from rspec-core.
If you choose so, disable this conversion by either:
- Specify
--keep itsoption manually. - Require
rspec-itsin your spec so that Transpec automatically disables this conversion.
- Conversion can be disabled by:
--keep its - Deprecation: Deprecated since RSpec 2.99, removed in RSpec 3.0
- See also: Core: its will be moved into an external gem - The Plan for RSpec 3
Current example object
This conversion is available only if your project has rspec gem dependency 2.99.0.beta1 or later.
# Targets
module ScreenshotHelper
def save_failure_screenshot
return unless example.exception
# ...
end
end
describe 'example page' do
include ScreenshotHelper
after { save_failure_screenshot }
let(:user) { User.find(example.[:user_id]) }
# ...
end
# Converted
module ScreenshotHelper
def save_failure_screenshot
return unless RSpec.current_example.exception
# ...
end
end
describe 'example page' do
include ScreenshotHelper
after { save_failure_screenshot }
let(:user) { |example| User.find(example.[:user_id]) }
# ...
end
Here's an excerpt from the warning for RSpec::Core::ExampleGroup#example and #running_example in RSpec 2.99:
RSpec::Core::ExampleGroup#exampleis deprecated and will be removed in RSpec 3. There are a few options for what you can use instead:
rspec-core's DSL methods (it,before,after,let,subject, etc) now yield the example as a block argument, and that is the recommended way to access the current example from those contexts.- The current example is now exposed via
RSpec.current_example, which is accessible from any context.- If you can't update the code at this call site (e.g. because it is in an extension gem), you can use this snippet to continue making this method available in RSpec 2.99 and RSpec 3:
RSpec.configure do |c| c.expose_current_running_example_as :example end
- Conversion can be disabled by:
--keep deprecated - Deprecation: Deprecated since RSpec 2.99, removed in RSpec 3.0
- See also: Core: DSL methods will yield the example - The Plan for RSpec 3
Compatibility
Tested on MRI 1.9, MRI 2.0 and JRuby in 1.9 mode.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
Copyright (c) 2013 Yuji Nakayama
See the LICENSE.txt for details.




