Class: Contract
- Inherits:
-
Test::Unit::TestCase
- Object
- Test::Unit::TestCase
- Contract
- Defined in:
- lib/contract.rb,
lib/contract/exception.rb,
lib/contract/overrides.rb,
lib/contract/assertions.rb,
lib/contract/integration.rb
Overview
Represents a contract between Objects as a collection of test cases. Objects are said to fulfill a contract if all test cases suceed. This is useful for ensuring that Objects your code is getting behave in a way that you expect them to behave so you can fail early or execute different logic for Objects with different interfaces.
The tests of the test suite will be run on a copy of the tested Object so you can safely test its behavior without having to fear data loss. By default Contracts obtain deep copies of Objects by serializing and unserializing them with Ruby’s Marshal
functionality. This will work in most cases but can fail for Objects containing unserializable parts like Procs, Files or Sockets. In those cases it is currently of your responsibility to provide a fitting implementation by overwriting the Contract.deep_copy method. In the future the contract library might provide different implementations of it via Ruby’s mixin mechanism.
Defined Under Namespace
Modules: Check, ContractException, SuiteMixin Classes: ContractError, ContractMismatch
Constant Summary collapse
- Version =
The Version of the contract library you are using as String of the 1.2.3 form where the digits stand for release, major and minor version respectively.
"0.1.1"
Class Attribute Summary collapse
-
.adaptions ⇒ Object
All adaption routes.
-
.check_fulfills ⇒ Object
(also: check_fulfills?)
Whether fulfills should be checked.
-
.check_signatures ⇒ Object
(also: check_signatures?)
Whether signatures should be checked.
-
.implications ⇒ Object
readonly
:nodoc:.
Class Method Summary collapse
-
.adapt(object, type) ⇒ Object
Tries to adapt the specified object to the specified type.
-
.deep_copy(object) ⇒ Object
This method is used internally for getting a copy of Objects that the contract is checked against.
-
.enforce(object) ⇒ Object
Enforces that object implements this contract.
-
.error_to_exception(error, object, contract) ⇒ Object
Maps a Test::Unit::Error instance to an actual Exception with the specified meta data.
-
.extract_method_name(test_name) ⇒ Object
Extracts the method name from a Test::Unit test_name style String.
-
.failure_to_exception(failure, object, contract) ⇒ Object
Maps a Test::Unit::Failure instance to an actual Exception with the specified meta data.
-
.fault_to_exception(fault, *args) ⇒ Object
Maps a Test::Unit fault (either a Failure or Error) to an actual exception with the specified meta data.
-
.fulfilled_by?(object) ⇒ Boolean
(also: ===)
Returns true if the given object fulfills this contract.
-
.implies(*mixins) ⇒ Object
Fulfilling this Contract (via Module#fulfills) implies that the Object is automatically compatible with the specified mixins which will then be included automatically.
-
.provides(*symbols, &block) ⇒ Object
Tests that the tested Object provides the specified methods with the specified behavior.
-
.suite ⇒ Object
We need to override a few methods of the suite.
-
.test(object, return_all = false) ⇒ Object
Tests whether the given Object fulfils this contract.
-
.test_all(object) ⇒ Object
Same as Contract.test, but will return all reasons for the Object not fulfilling the contract in an Array or nil in case of fulfillment.
Instance Method Summary collapse
-
#default_test ⇒ Object
Having empty contracts makes sense and is not an unexpected situation.
-
#run(result, object = (no_object = true)) ⇒ Object
We need to run the test suite against a specific Object.
Class Attribute Details
.adaptions ⇒ Object
All adaption routes.
110 111 112 |
# File 'lib/contract/integration.rb', line 110 def adaptions @adaptions end |
.check_fulfills ⇒ Object Also known as: check_fulfills?
Whether fulfills should be checked. This is enabled by default.
Note: If you want to change this you need to do so before doing any Module#fulfills calls or it will not be applied. It’s probably best set right after requiring the contract library.
106 107 108 |
# File 'lib/contract/integration.rb', line 106 def check_fulfills @check_fulfills end |
.check_signatures ⇒ Object Also known as: check_signatures?
Whether signatures should be checked. By default signatures are checked only when the application is run in $DEBUG mode. (By specifying the -d switch on the invocation of Ruby.)
Note: If you want to change this you need to do so before doing any Module#signature calls or it will not be applied. It’s probably best set right after requiring the contract library.
98 99 100 |
# File 'lib/contract/integration.rb', line 98 def check_signatures @check_signatures end |
.implications ⇒ Object (readonly)
:nodoc:
132 133 134 |
# File 'lib/contract.rb', line 132 def implications @implications end |
Class Method Details
.adapt(object, type) ⇒ Object
Tries to adapt the specified object to the specified type. Returns the old object if no suitable adaption route was found or if it already is of the specified type.
This will only use adaptions where the :to part is equal to the specified type. No multi-step conversion will be performed.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/contract/integration.rb', line 124 def self.adapt(object, type) return object if type === object @adaptions[type].each do |adaption| if adaption[:from] === object and (adaption[:if].nil? or adaption[:if] === object) then result = adaption[:via].call(object) return result if type === result end end return object end |
.deep_copy(object) ⇒ Object
This method is used internally for getting a copy of Objects that the contract is checked against. By default it uses Ruby’s Marshal
functionality for obtaining a copy, but this can fail if the Object contains unserializable parts like Procs, Files or Sockets. It is currently your responsibility to provide a fitting implementation of this by overwriting the method in case the default implementation does not work for you. In the future the contract library might offer different implementations for this via Ruby’s mixin mechanism.
104 105 106 |
# File 'lib/contract.rb', line 104 def self.deep_copy(object) Marshal.load(Marshal.dump(object)) end |
.enforce(object) ⇒ Object
Enforces that object implements this contract. If it does not an Exception will be raised. This is useful for example useful when you need to ensure that the arguments given to a method fulfill a given contract.
Note that using Module#enforce is a higher-level way of checking arguments and return values for the conformance of a given type. You might however still want to use Contract.enforce directly when you need more flexibility.
65 66 67 68 |
# File 'lib/contract.rb', line 65 def self.enforce(object) reason = self.test(object) raise reason if reason end |
.error_to_exception(error, object, contract) ⇒ Object
Maps a Test::Unit::Error instance to an actual Exception with the specified meta data.
75 76 77 78 79 |
# File 'lib/contract/exception.rb', line 75 def self.error_to_exception(error, object, contract) # :nodoc: original = error.exception ContractError.new(original., original.backtrace, object, extract_method_name(error.test_name), contract, original.class) end |
.extract_method_name(test_name) ⇒ Object
Extracts the method name from a Test::Unit test_name style String.
92 93 94 |
# File 'lib/contract/exception.rb', line 92 def self.extract_method_name(test_name) # :nodoc: test_name[/\A(.+?)\(.+?\)\Z/, 1] end |
.failure_to_exception(failure, object, contract) ⇒ Object
Maps a Test::Unit::Failure instance to an actual Exception with the specified meta data.
68 69 70 71 |
# File 'lib/contract/exception.rb', line 68 def self.failure_to_exception(failure, object, contract) # :nodoc: ContractMismatch.new(failure., failure.location, object, extract_method_name(failure.test_name), contract) end |
.fault_to_exception(fault, *args) ⇒ Object
Maps a Test::Unit fault (either a Failure or Error) to an actual exception with the specified meta data.
83 84 85 86 87 88 89 |
# File 'lib/contract/exception.rb', line 83 def self.fault_to_exception(fault, *args) # :nodoc: if fault.is_a?(Test::Unit::Failure) then failure_to_exception(fault, *args) else error_to_exception(fault, *args) end end |
.fulfilled_by?(object) ⇒ Boolean Also known as: ===
Returns true if the given object fulfills this contract. This is useful for implementing dispatching mechanisms where you want to hit different code branches based on whether an Object has one or another interface.
42 43 44 |
# File 'lib/contract.rb', line 42 def self.fulfilled_by?(object) self.test(object).nil? end |
.implies(*mixins) ⇒ Object
Fulfilling this Contract (via Module#fulfills) implies that the Object is automatically compatible with the specified mixins which will then be included automatically. For example the Enumerable relationship could be expressed like this:
class EnumerableContract < Contract
provides :each
implies Enumerable
end
117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/contract.rb', line 117 def self.implies(*mixins) mixins.each do |mixin| if not mixin.is_a?(Module) then raise(TypeError, "wrong argument type #{mixin.class} for " + "#{mixin.inspect} (expected Module)") end end @implications ||= Array.new @implications += mixins end |
.provides(*symbols, &block) ⇒ Object
Tests that the tested Object provides the specified methods with the specified behavior.
If a block is supplied it will be evaluated in the context of the contract so @object
will refer to the object being tested.
This can be used like this:
class ListContract < Contract
provides :size do
assert(@object.size >= 0, "#size should never be negative.")
end
provides :include?
provides :each do
count = 0
@object.each do |item|
assert(@object.include?(item),
"#each should only yield items that the list includes.")
count += 1
end
assert_equal(@object.size, count,
"#each should yield #size items.")
end
end
34 35 36 37 38 39 40 41 |
# File 'lib/contract/assertions.rb', line 34 def self.provides(*symbols, &block) # :yields: symbols.each do |symbol| define_method("test_provides_#{symbol}".intern) do assert_respond_to(@object, symbol) instance_eval(&block) if block end end end |
.suite ⇒ Object
We need to override a few methods of the suite. (See SuiteMixin)
22 23 24 25 26 |
# File 'lib/contract/overrides.rb', line 22 def self.suite() # :nodoc: result = super() result.extend SuiteMixin return result end |
.test(object, return_all = false) ⇒ Object
Tests whether the given Object fulfils this contract.
Note: This will return the first reason for the Object not fulfilling the contract or nil
in case it fulfills it.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/contract.rb', line 74 def self.test(object, return_all = false) reasons = [] result = Test::Unit::TestResult.new result.add_listener(Test::Unit::TestResult::FAULT) do |fault| reason = Contract.fault_to_exception(fault, object, self) return reason unless return_all reasons << reason end self.suite.run(result, deep_copy(object)) return reasons unless result.passed? end |
.test_all(object) ⇒ Object
Same as Contract.test, but will return all reasons for the Object not fulfilling the contract in an Array or nil in case of fulfillment. (as an Array of Exceptions) or nil
in the case it does fulfill it.
92 93 94 |
# File 'lib/contract.rb', line 92 def self.test_all(object) test(object, true) end |
Instance Method Details
#default_test ⇒ Object
Having empty contracts makes sense and is not an unexpected situation.
39 40 |
# File 'lib/contract/overrides.rb', line 39 def default_test() # :nodoc: end |
#run(result, object = (no_object = true)) ⇒ Object
We need to run the test suite against a specific Object.
29 30 31 32 33 34 35 36 |
# File 'lib/contract/overrides.rb', line 29 def run(result, object = (no_object = true)) # :nodoc: # The test auto runners might try to invoke this. In that case we just # do nothing. unless no_object @object = object super(result) end end |