mirror of
				https://github.com/ruby/ruby.git
				synced 2022-11-09 12:17:21 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			243 lines
		
	
	
	
		
			7.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
	
		
			7.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
Contributions are much appreciated.
 | 
						|
Please open a pull request or add an issue to discuss what you intend to work on.
 | 
						|
If the pull requests passes the CI and conforms to the existing style of specs, it will be merged.
 | 
						|
 | 
						|
### File organization
 | 
						|
 | 
						|
Spec are grouped in 5 separate top-level groups:
 | 
						|
 | 
						|
* `command_line`: for the ruby executable command-line flags (`-v`, `-e`, etc)
 | 
						|
* `language`: for the language keywords and syntax constructs (`if`, `def`, `A::B`, etc)
 | 
						|
* `core`: for the core methods (`Fixnum#+`, `String#upcase`, no need to require anything)
 | 
						|
* `library`: for the standard libraries methods (`CSV.new`, `YAML.parse`, need to require the stdlib)
 | 
						|
* `optional/capi`: for functions available to the Ruby C-extension API
 | 
						|
 | 
						|
The exact file for methods is decided by the `#owner` of a method, for instance for `#group_by`:
 | 
						|
```ruby
 | 
						|
> [].method(:group_by)
 | 
						|
=> #<Method: Array(Enumerable)#group_by>
 | 
						|
> [].method(:group_by).owner
 | 
						|
=> Enumerable
 | 
						|
```
 | 
						|
Which should therefore be specified in `core/enumerable/group_by_spec.rb`.
 | 
						|
 | 
						|
### MkSpec - a tool to generate the spec structure
 | 
						|
 | 
						|
If you want to create new specs, you should use `mkspec`, part of [MSpec](http://github.com/ruby/mspec).
 | 
						|
 | 
						|
    $ ../mspec/bin/mkspec -h
 | 
						|
 | 
						|
#### Creating files for unspecified modules or classes
 | 
						|
 | 
						|
For instance, to create specs for `forwardable`:
 | 
						|
 | 
						|
    $ ../mspec/bin/mkspec -b library -rforwardable -c Forwardable
 | 
						|
 | 
						|
Specify `core` or `library` as the `base`.
 | 
						|
 | 
						|
#### Finding unspecified core methods
 | 
						|
 | 
						|
This is very easy, just run the command below in your `spec` directory.
 | 
						|
`ruby` must be a recent version of MRI.
 | 
						|
 | 
						|
    $ ruby --disable-gem ../mspec/bin/mkspec
 | 
						|
 | 
						|
You might also want to search for:
 | 
						|
 | 
						|
    it "needs to be reviewed for spec completeness"
 | 
						|
 | 
						|
which indicates the file was generated but the method unspecified.
 | 
						|
 | 
						|
### Matchers and expectations
 | 
						|
 | 
						|
Here is a list of frequently-used matchers, which should be enough for most specs.
 | 
						|
There are a few extra specific matchers used in the couple specs that need it.
 | 
						|
 | 
						|
```ruby
 | 
						|
(1 + 2).should == 3 # Calls #==
 | 
						|
(1 + 2).should_not == 5
 | 
						|
 | 
						|
File.should equal(File) # Calls #equal? (tests identity)
 | 
						|
(1 + 2).should eql(3) # Calls #eql? (Hash equality)
 | 
						|
 | 
						|
1.should < 2
 | 
						|
2.should <= 2
 | 
						|
3.should >= 3
 | 
						|
4.should > 3
 | 
						|
 | 
						|
"Hello".should =~ /l{2}/ # Calls #=~ (Regexp match)
 | 
						|
 | 
						|
[].should be_empty # Calls #empty?
 | 
						|
[1,2,3].should include(2) # Calls #include?
 | 
						|
 | 
						|
(0.1 + 0.2).should be_close(0.3, TOLERANCE) # (0.2-0.1).abs < TOLERANCE
 | 
						|
(0.0/0.0).should be_nan # Calls Float#nan?
 | 
						|
(1.0/0.0).should be_positive_infinity
 | 
						|
(-1.0/0.0).should be_negative_infinity
 | 
						|
 | 
						|
3.14.should be_an_instance_of(Float) # Calls #instance_of?
 | 
						|
3.14.should be_kind_of(Numeric) # Calls #is_a?
 | 
						|
Numeric.should be_ancestor_of(Float) # Float.ancestors.include?(Numeric)
 | 
						|
 | 
						|
3.14.should respond_to(:to_i) # Calls #respond_to?
 | 
						|
Fixnum.should have_instance_method(:+)
 | 
						|
Array.should have_method(:new)
 | 
						|
# Also have_constant, have_private_instance_method, have_singleton_method, etc
 | 
						|
 | 
						|
-> {
 | 
						|
  raise "oops"
 | 
						|
}.should raise_error(RuntimeError, /oops/)
 | 
						|
 | 
						|
# To avoid! Instead, use an expectation testing what the code in the lambda does.
 | 
						|
# If an exception is raised, it will fail the example anyway.
 | 
						|
-> { ... }.should_not raise_error
 | 
						|
 | 
						|
-> {
 | 
						|
  Fixnum
 | 
						|
}.should complain(/constant ::Fixnum is deprecated/) # Expect a warning
 | 
						|
```
 | 
						|
 | 
						|
### Guards
 | 
						|
 | 
						|
Different guards are available as defined by mspec.
 | 
						|
Here is a list of the most commonly-used guards:
 | 
						|
 | 
						|
```ruby
 | 
						|
ruby_version_is ""..."2.4" do
 | 
						|
  # Specs for RUBY_VERSION < 2.4
 | 
						|
end
 | 
						|
 | 
						|
ruby_version_is "2.4" do
 | 
						|
  # Specs for RUBY_VERSION >= 2.4
 | 
						|
end
 | 
						|
 | 
						|
platform_is :windows do
 | 
						|
  # Specs only valid on Windows
 | 
						|
end
 | 
						|
 | 
						|
platform_is_not :windows do
 | 
						|
  # Specs valid on platforms other than Windows
 | 
						|
end
 | 
						|
 | 
						|
platform_is :linux, :darwin do # OR
 | 
						|
end
 | 
						|
 | 
						|
platform_is_not :linux, :darwin do # Not Linux and not Darwin
 | 
						|
end
 | 
						|
 | 
						|
platform_is wordsize: 64 do
 | 
						|
  # 64-bit platform
 | 
						|
end
 | 
						|
 | 
						|
big_endian do
 | 
						|
  # Big-endian platform
 | 
						|
end
 | 
						|
 | 
						|
# In case there is a bug in MRI but the expected behavior is obvious
 | 
						|
# First file a bug at https://bugs.ruby-lang.org/
 | 
						|
# It is better to use a ruby_version_is guard if there was a release with the fix
 | 
						|
ruby_bug '#13669', ''...'2.5' do
 | 
						|
  it "works like this" do
 | 
						|
    # Specify the expected behavior here, not the bug
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
# Combining guards
 | 
						|
guard -> { platform_is :windows and ruby_version_is ""..."2.5" } do
 | 
						|
  # Windows and RUBY_VERSION < 2.5
 | 
						|
end
 | 
						|
 | 
						|
guard_not -> { platform_is :windows and ruby_version_is ""..."2.5" } do
 | 
						|
  # The opposite
 | 
						|
end
 | 
						|
 | 
						|
# Custom guard
 | 
						|
max_uint = (1 << 32) - 1
 | 
						|
guard -> { max_uint <= fixnum_max } do
 | 
						|
end
 | 
						|
```
 | 
						|
 | 
						|
Custom guards are better than a simple `if` as they allow [mspec commands](https://github.com/ruby/mspec/issues/30#issuecomment-312487779) to work properly.
 | 
						|
 | 
						|
In general, the usage of guards should be minimized as possible.
 | 
						|
 | 
						|
There are no guards to define implementation-specific behavior because
 | 
						|
the Ruby Spec Suite defines common behavior and not implementation details.
 | 
						|
Use the implementation test suite for these.
 | 
						|
 | 
						|
If an implementation does not support some feature, simply tag the related specs as failing instead.
 | 
						|
 | 
						|
### Shared Specs
 | 
						|
 | 
						|
Often throughout Ruby, identical functionality is used by different methods and modules. In order
 | 
						|
to avoid duplication of specs, we have shared specs that are re-used in other specs.  The use is a
 | 
						|
bit tricky however, so let's go over it.
 | 
						|
 | 
						|
Commonly, if a shared spec is only reused within its own module, the shared spec will live within a
 | 
						|
shared directory inside that module's directory. For example, the `core/hash/shared/key.rb` spec is
 | 
						|
only used by `Hash` specs, and so it lives inside `core/hash/shared/`.
 | 
						|
 | 
						|
When a shared spec is used across multiple modules or classes, it lives within the `shared/` directory.
 | 
						|
An example of this is the `shared/file/socket.rb` which is used by `core/file/socket_spec.rb`,
 | 
						|
`core/filetest/socket_spec.rb`, and `core/file/state/socket_spec.rb` and so it lives in the root `shared/`.
 | 
						|
 | 
						|
Defining a shared spec involves adding a `shared: true` option to the top-level `describe` block. This
 | 
						|
will signal not to run the specs directly by the runner.  Shared specs have access to two instance
 | 
						|
variables from the implementor spec: `@method` and `@object`, which the implementor spec will pass in.
 | 
						|
 | 
						|
Here's an example of a snippet of a shared spec and two specs which integrates it:
 | 
						|
 | 
						|
```ruby
 | 
						|
# core/hash/shared/key.rb
 | 
						|
describe :hash_key_p, shared: true do
 | 
						|
  it "returns true if the key's matching value was false" do
 | 
						|
    { xyz: false }.send(@method, :xyz).should == true
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
# core/hash/key_spec.rb
 | 
						|
describe "Hash#key?" do
 | 
						|
  it_behaves_like :hash_key_p, :key?
 | 
						|
end
 | 
						|
 | 
						|
# core/hash/include_spec.rb
 | 
						|
describe "Hash#include?" do
 | 
						|
  it_behaves_like :hash_key_p, :include?
 | 
						|
end
 | 
						|
```
 | 
						|
 | 
						|
In the example, the first `describe` defines the shared spec `:hash_key_p`, which defines a spec that
 | 
						|
calls the `@method` method with an expectation.  In the implementor spec, we use `it_behaves_like` to
 | 
						|
integrate the shared spec.  `it_behaves_like` takes 3 parameters: the key of the shared spec, a method,
 | 
						|
and an object.  These last two parameters are accessible via `@method` and `@object` in the shared spec.
 | 
						|
 | 
						|
Sometimes, shared specs require more context from the implementor class than a simple object. We can address
 | 
						|
this by passing a lambda as the method, which will have the scope of the implementor.  Here's an example of
 | 
						|
how this is used currently:
 | 
						|
 | 
						|
```ruby
 | 
						|
describe :kernel_sprintf, shared: true do
 | 
						|
  it "raises TypeError exception if cannot convert to Integer" do
 | 
						|
    -> { @method.call("%b", Object.new) }.should raise_error(TypeError)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
describe "Kernel#sprintf" do
 | 
						|
  it_behaves_like :kernel_sprintf, -> (format, *args) {
 | 
						|
    sprintf(format, *args)
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
describe "Kernel.sprintf" do
 | 
						|
  it_behaves_like :kernel_sprintf, -> (format, *args) {
 | 
						|
    Kernel.sprintf(format, *args)
 | 
						|
  }
 | 
						|
end
 | 
						|
```
 | 
						|
 | 
						|
In the above example, the method being passed is a lambda that triggers the specific conditions of the shared spec.
 | 
						|
 | 
						|
### Style
 | 
						|
 | 
						|
Do not leave any trailing space and follow the existing style.
 |