Class: Contract

Inherits:
Test::Unit::TestCase
  • Object
show all
Defined in:
lib/carat-dev/interface_work/contracts/contract/lib/contract.rb,
lib/carat-dev/interface_work/contracts/contract/lib/contract/exception.rb,
lib/carat-dev/interface_work/contracts/contract/lib/contract/overrides.rb,
lib/carat-dev/interface_work/contracts/contract/lib/contract/assertions.rb,
lib/carat-dev/interface_work/contracts/contract/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: ContractException Classes: ContractError, ContractMismatch

Constant Summary collapse

Version =

The Version of the contract library you are using.

id.split(" ")[2].to_i

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.check_signaturesObject 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 signature calls or it will not be applied.



13
14
15
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/integration.rb', line 13

def check_signatures
  @check_signatures
end

Class Method Details

.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.



85
86
87
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract.rb', line 85

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.



46
47
48
49
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract.rb', line 46

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.



72
73
74
75
76
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/exception.rb', line 72

def self.error_to_exception(error, object, contract) # :nodoc:
  original = error.exception
  ContractError.new(original.message, 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.



89
90
91
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/exception.rb', line 89

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.



65
66
67
68
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/exception.rb', line 65

def self.failure_to_exception(failure, object, contract) # :nodoc:
  ContractMismatch.new(failure.message, 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.



80
81
82
83
84
85
86
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/exception.rb', line 80

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.

Returns:

  • (Boolean)


34
35
36
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract.rb', line 34

def self.fulfilled_by?(object)
  self.test(object).nil?
end

.provides(*symbols, &block) ⇒ Object

Tests that the tested Object provides the specified methods with the specified behavior.

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


29
30
31
32
33
34
35
36
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/assertions.rb', line 29

def self.provides(*symbols, &block)
  symbols.each do |symbol|
    define_method(:"test_provides_#{symbol}") do
      assert_respond_to(@object, symbol)
      instance_eval(&block) if block
    end
  end
end

.suiteObject

The resulting suite() should pass along additional parameters that are given to the suite.run method so we can supply a specific Object to run the test suite against.



10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/overrides.rb', line 10

def self.suite() # :nodoc:
  result = super()
  def result.run(result, *more, &progress_block)
    progress_block ||= lambda { |*args| }
    progress_block.call(STARTED, name)
    @tests.each do |test|
      test.run(result, *more, &progress_block)
    end
    progress_block.call(FINISHED, name)
  end
  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.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract.rb', line 55

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, 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.



73
74
75
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract.rb', line 73

def self.test_all(object)
  test(object, true)
end

Instance Method Details

#default_testObject

Having empty contracts makes sense and is not an unexpected situation.



30
31
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/overrides.rb', line 30

def default_test() # :nodoc:
end

#run(result, object) ⇒ Object

We need to run the test suite against a specific Object.



24
25
26
27
# File 'lib/carat-dev/interface_work/contracts/contract/lib/contract/overrides.rb', line 24

def run(result, object) # :nodoc:
  @object = object
  super(result)
end