Module: NRSER::RSpex::ExampleGroup

Defined in:
lib/nrser/rspex/example_group.rb,
lib/nrser/rspex/example_group/describe_x.rb,
lib/nrser/rspex/example_group/describe_when.rb,
lib/nrser/rspex/example_group/describe_setup.rb,
lib/nrser/rspex/example_group/describe_instance.rb,
lib/nrser/rspex/example_group/describe_use_case.rb,
lib/nrser/rspex/example_group/describe_spec_file.rb,
lib/nrser/rspex/example_group/describe_instance_method.rb

Overview

Instance methods to extend examples groups with. Also included globally so they’re available at the top-level in files.

Instance Method Summary collapse

Instance Method Details

#context_where(description = nil, **bindings, &body) ⇒ Object

Define a ‘context` block with `let` bindings and evaluate the `body` block in it.

Parameters:

  • **bindings (Hash<Symbol, Object>)

    Map of symbol names to value to bind using ‘let`.

  • &body (#call)

    Body block to evaluate in the context.

Returns:

  • Whatever ‘context` returns.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/nrser/rspex/example_group.rb', line 242

def context_where description = nil, **bindings, &body
  
  if description.nil?
    description = bindings.map { |name, value|
      "#{ name }: #{ NRSER::RSpex.short_s value }"
    }.join( ", " )
  end
  
  context "#{ description }", type: :where do
    bindings.each { |name, value|
      let( name ) { unwrap value, context: self }
    }
    
    module_exec &body
  end
end

#describe_attribute(symbol, **metadata, &block) ⇒ Object Also known as: describe_attr



215
216
217
218
219
220
221
222
223
224
# File 'lib/nrser/rspex/example_group.rb', line 215

def describe_attribute symbol, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:attribute] } ##{ symbol }",
    type: :attribute,
    **
  ) do
    subject { super().public_send symbol }
    module_exec &block
  end
end

#describe_called_with(*args, &body) ⇒ Object Also known as: called_with, when_called_with

Create a new RSpec.describe section where the subject is set by calling the parent subject with ‘args` and evaluate `block` in it.

Examples:

describe "hi sayer" do
  subject{ ->( name ) { "Hi #{ name }!" } }

  describe_called_with 'Mom' do
    it { is_expected.to eq 'Hi Mom!' }
  end
end

Parameters:

  • *args (Array)

    Arguments to call ‘subject` with to produce the new subject.

  • &block (#call)

    Block to execute in the context of the example group after refining the subject.



28
29
30
31
32
33
# File 'lib/nrser/rspex/example_group.rb', line 28

def describe_called_with *args, &body
  describe_x_type  "called with", List(*args),
    type: :invocation,
    subject_block: -> { super().call *args },
    &body
end

#describe_class(klass, bind_subject: true, **metadata, &block) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/nrser/rspex/example_group.rb', line 165

def describe_class klass, bind_subject: true, **, &block
  description = "#{ NRSER::RSpex::PREFIXES[:class] } #{ klass.name }"
  
  describe(
    description,
    type: :class,
    class: klass,
    **
  ) do
    if bind_subject
      subject { klass }
    end
    
    module_exec &block
  end
end

#describe_file(path, **metadata, &body) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/nrser/rspex/example_group.rb', line 135

def describe_file path, **, &body
  title = path
  
  describe(
    "#{ NRSER::RSpex::PREFIXES[:file] } #{ title }",
    type: :file,
    file: path,
    **
  ) do
    module_exec &body
  end
end

#describe_group(title, **metadata, &block) ⇒ Object



188
189
190
191
192
193
194
195
196
# File 'lib/nrser/rspex/example_group.rb', line 188

def describe_group title, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:group] } #{ title }",
    type: :group,
    **
  ) do
    module_exec &block
  end
end

#describe_instance(*constructor_args, &body) ⇒ return_type

TODO:

Document describe_instance method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



13
14
15
16
17
18
19
20
21
22
# File 'lib/nrser/rspex/example_group/describe_instance.rb', line 13

def describe_instance *constructor_args, &body
  describe_x_type ".new(", Args(*constructor_args), ")",
    type: :instance,
    metadata: {
      constructor_args: constructor_args,
    },
    # subject_block: -> { super().new *described_args },
    subject_block: -> { super().new *described_constructor_args },
    &body
end

#describe_instance_method(name, **metadata, &block) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/nrser/rspex/example_group/describe_instance_method.rb', line 5

def describe_instance_method name, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:method] } #{ name }",
    type: :method,
    method_name: name,
    **
  ) do
    if name.is_a? Symbol
      subject { super().method name }
    end
    
    module_exec &block
  end
end

#describe_message(symbol, *args, &body) ⇒ Object



44
45
46
47
48
49
50
51
52
# File 'lib/nrser/rspex/example_group.rb', line 44

def describe_message symbol, *args, &body
  description = \
    "message #{ [symbol, *args].map( &NRSER::RSpex.method( :short_s ) ).join( ', ' ) }"
  
  describe description, type: :message do
    subject { NRSER::Message.new symbol, *args }
    module_exec &body
  end
end

#describe_method(name, **metadata, &block) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/nrser/rspex/example_group.rb', line 199

def describe_method name, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:method] } #{ name }",
    type: :method,
    method_name: name,
    **
  ) do
    if name.is_a? Symbol
      subject { super().method name }
    end
    
    module_exec &block
  end
end

#describe_module(mod, bind_subject: true, **metadata, &block) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/nrser/rspex/example_group.rb', line 149

def describe_module mod, bind_subject: true, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:module] } #{ mod.name }",
    type: :module,
    module: mod,
    **
  ) do
    if bind_subject
      subject { mod }
    end
    
    module_exec &block
  end
end

#describe_return_value(*args, &body) ⇒ Object



91
92
93
94
95
96
97
98
# File 'lib/nrser/rspex/example_group.rb', line 91

def describe_return_value *args, &body
  msg = NRSER::Message.from *args
  
  describe "return value from #{ msg }" do
    subject { msg.send_to super() }
    module_exec &body
  end # "return value from #{ msg }"
end

#describe_section(title, **metadata, &block) ⇒ Object Also known as: describe_topic

Describe a “section”. Just like RSpec.describe except it:

  1. Expects a string title.

  2. Prepends a little section squiggle ‘§` to the title so sections are easier to pick out visually.

  3. Adds ‘type: :section` metadata.

Parameters:

Returns:

  • Whatever RSpec.describe returns.



121
122
123
124
125
126
127
128
129
# File 'lib/nrser/rspex/example_group.rb', line 121

def describe_section title, **, &block
  describe(
    "#{ NRSER::RSpex::PREFIXES[:section] } #{ title }",
    type: :section,
    **
  ) do
    module_exec &block
  end
end

#describe_sent_to(receiver, publicly: true, &block) ⇒ Object Also known as: sent_to, when_sent_to

For use when ‘subject` is a Message. Create a new context for the `receiver` where the subject is the result of sending that message to the receiver.

Parameters:

  • receiver (Object)

    Object that will receive the message to create the new subject.

  • publicly: (Boolean) (defaults to: true)

    Send message publicly via Object#public_send (default) or privately via Object.send.

Returns:

  • Whatever the ‘context` call returns.



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/nrser/rspex/example_group.rb', line 69

def describe_sent_to receiver, publicly: true, &block
  mode = if publicly
    "publicly"
  else
    "privately"
  end
  
  describe "sent to #{ receiver } (#{ mode })" do
    subject { super().send_to unwrap( receiver, context: self ) }
    module_exec &block
  end
end

#describe_setup(*description, **metadata, &body) ⇒ void Also known as: setup

This method returns an undefined value.

Setup describes what’s going to be done in all child examples.

It’s where you setup your ‘subject`, usually depending on `let` bindings that are provided in the children.



12
13
14
15
16
17
18
# File 'lib/nrser/rspex/example_group/describe_setup.rb', line 12

def describe_setup *description, **, &body
  describe_x \
    *description,
    type: :setup,
    metadata: ,
    &body
end

#describe_spec_file(description: nil, spec_path:, bind_subject: true, **metadata, &body) ⇒ nil

TODO:

This is totally just a one-off right now… would need to be generalized quite a bit…

  1. Extraction of module, class, etc from metadata should be flexible

  2. Built description would need to be conditional on what metadata was found.

EXPERIMENTAL

Example group helper for use at the top level of each spec file to set a bunch of stuff up and build a helpful description.

Parameters:

  • description: (String) (defaults to: nil)

    A description of the spec file to add to the RSpec description.

  • spec_path: (String)

    The path to the spec file (just feed it ‘__FILE__`).

    Probably possible to extract this somehow without having to provide it?

Returns:

  • (nil)


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/nrser/rspex/example_group/describe_spec_file.rb', line 29

def describe_spec_file  description: nil,
                        spec_path:,
                        bind_subject: true,
                        **,
                        &body
  
  if [:module] && [:method]
    meth = [:module].method [:method]
    file, line = meth.source_location
    path = Pathname.new file
    loc = "./#{ path.relative_path_from Pathname.getwd }:#{ line }"
    
    spec_rel_path = \
      "./#{ Pathname.new( spec_path ).relative_path_from Pathname.getwd }"
    
    desc = [
      "#{ [:module].name }.#{ [:method] }",
      "(#{ loc })",
      description,
      "Spec (#{ spec_rel_path})"
    ].compact.join " "
    
    subj = meth
  
  elsif [:class]
    klass = [:class]
    
    if [:instance_method]
      instance_method = klass.instance_method [:instance_method]
      
      file, line = instance_method.source_location
      
      name = "#{ klass.name }##{ [:instance_method] }"
    else
      name = klass.name
      
      # Get a reasonable file and line for the class
      file, line = klass.
        # Get an array of all instance methods, excluding inherited ones
        # (the `false` arg)
        instance_methods( false ).
        # Add `#initialize` since it isn't in `#instance_methods` for some
        # reason
        <<( :initialize ).
        # Map those to their {UnboundMethod} objects
        map { |sym| klass.instance_method sym }.
        # Toss any `nil` values
        compact.
        # Get the source locations
        map( &:source_location ).
        # Get the first line in the shortest path
        min_by { |(path, line)| [path.length, line] }
        
        # Another approach I thought of... (untested)
        # 
        # Get the path
        # # Get frequency of the paths
        # count_by { |(path, line)| path }.
        # # Get the one with the most occurrences
        # max_by { |path, count| count }.
        # # Get just the path (not the count)
        # first
    end
    
    location = if file
      "(#{ NRSER::RSpex.dot_rel_path file }:#{ line })"
    end
    
    desc = [
      "𝑆𝑃𝐸𝐶 𝐹𝐼𝐿𝐸 `#{ NRSER::RSpex.dot_rel_path spec_path }` 𝐹𝑂𝑅",
      name,
      location,
      description,
    ].compact.join " "
    
    subj = klass
    
  else
    # TODO  Make this work!
    raise ArgumentError.new binding.erb <<-END
      Not yet able to handle metadata:
      
          <%= metadata.pretty_inspect %>
      
    END
  end
  
  describe desc, ** do
    if bind_subject
      subject { subj }
    end
    
    module_exec &body
  end
  
  nil
end

#describe_use_case(*description, where: {}, **metadata, &body) ⇒ void

TODO:

Document describe_use_case method.

This method returns an undefined value.



9
10
11
12
13
14
15
16
# File 'lib/nrser/rspex/example_group/describe_use_case.rb', line 9

def describe_use_case *description, where: {}, **, &body
  describe_x \
    *description,
    type: :use_case,
    bindings: where,
    metadata: ,
    &body
end

#describe_when(*description, **bindings, &body) ⇒ Object

Define a example group block with ‘let` bindings and evaluate the `body` block in it.

Parameters:

  • **bindings (Hash<Symbol, Object>)

    Map of symbol names to value to bind using ‘let`.

  • &body (#call)

    Body block to evaluate in the context.

Returns:

  • Whatever ‘context` returns.



17
18
19
20
21
22
23
# File 'lib/nrser/rspex/example_group/describe_when.rb', line 17

def describe_when *description, **bindings, &body
  describe_x \
    *description,
    type: :when,
    bindings: bindings,
    &body
end

#describe_x(*description, type:, metadata: {}, bindings: {}, add_binding_desc: true, subject_block: nil, &body) ⇒ return_type Also known as: describe_x_type

The core, mostly internal method that all RSpex’s description methods lead back too (or should / will when refactoring is done).

Keyword options are explicitly broken out in this method, versus the sugary ones that call it, so ‘metadata` can be set without restriction (save the `type` key, which is also it’s own keyword here). You can use this method if you want the RSpex functionality but absolutely have to set some metadata key that we use for something else.

Parameters:

  • *description (Array)

    Optional list of elements that compose the custom description.

    Will be passed to Format.description to produce the string value that is in turn passed to RSpec.describe.

  • type: (Symbol)

    The RSpex “type” of the example group, which is used to determine the prefix of the final description and is assigned to the ‘:type` metadata key.

  • metadata: (Hash<Symbol, Object>) (defaults to: {})

    Metadata to add to the new example group.

    In addition to the keys RSpec will reject, we prohibit ‘:type` unless it is the same as the `type` keyword argument or `nil`.

    In either of these cases, the ‘type` keyword arg will be used for the new example group’s ‘:type` metadata value.

  • bindings: (Hash<Symbol, Object>) (defaults to: {})

Returns:

  • (return_type)

    @todo Document return value.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/nrser/rspex/example_group/describe_x.rb', line 40

def describe_x  *description,
                type:,
                metadata: {},
                bindings: {},
                add_binding_desc: true,
                subject_block: nil,
                &body
  
  # Check that `metadata` doesn't have a `:type` value too... although we
  # allow it if's equal to `type` or `nil` 'cause why not I guess?
  # 
  if  .key?( :type ) &&
      [:type] != nil &&
      [:type] != type
    raise ArgumentError.new binding.erb <<-END
      `metadata:` keyword argument may not have a `:type` key that conflicts
      with the `type:` keyword argument.
      
      Received:
        `type`:
        
            <%= type.inspect %>
        
        `metadata[:type]`:
        
            <%= metadata[:type].pretty_inspect %>
      
    END
  end
  
  unless bindings.empty? || add_binding_desc == false
    # bindings_desc = NRSER::RSpex::Opts[bindings].to_desc
    bindings_desc = ["(", bindings.ai( multiline: false ), ")"]
    
    if description.empty?
      description = bindings.ai( multiline: false )
    else
      description += ["(", bindings.ai( multiline: false ), ")"]
    end
  end
  
  formatted = NRSER::RSpex::Format.description *description, type: type
  
  describe formatted, **, type: type do
    subject( &subject_block ) if subject_block
    
    unless bindings.empty?
      bindings.each { |name, value|
        let( name ) { unwrap value, context: self }
      }
    end
    
    module_exec &body
  end # description,
  
end

#described_classObject



183
184
185
# File 'lib/nrser/rspex/example_group.rb', line 183

def described_class
  [:class] || super()
end