diff --git a/src/spectator/dsl/example_factory.cr b/src/spectator/dsl/example_factory.cr index 1610461..97345da 100644 --- a/src/spectator/dsl/example_factory.cr +++ b/src/spectator/dsl/example_factory.cr @@ -1,8 +1,13 @@ module Spectator::DSL + # Creates instances of examples from a specified class. class ExampleFactory + # Creates the factory. + # The type passed to this constructor must be a sub-type of `Example`. def initialize(@example_type : Example.class) end + # Constructs a new example instance and returns it. + # The `group` and `sample_valuees` are passed to `Example#initialize`. def build(group : ExampleGroup, sample_values : Internals::SampleValues) : Example @example_type.new(group, sample_values) end diff --git a/src/spectator/dsl/example_group_builder.cr b/src/spectator/dsl/example_group_builder.cr index 9d939e4..93dc958 100644 --- a/src/spectator/dsl/example_group_builder.cr +++ b/src/spectator/dsl/example_group_builder.cr @@ -1,38 +1,57 @@ module Spectator::DSL + # Base class for building all example groups. abstract class ExampleGroupBuilder + # Type alias for valid children of example groups. + # NOTE: `NestedExampleGroupBuilder` is used instead of `ExampleGroupBuilder`. + # That is because `RootExampleGroupBuilder` also inherits from this class, + # and the root example group can't be a child. alias Child = ExampleFactory | NestedExampleGroupBuilder + # Factories and builders for all examples and groups. @children = [] of Child + + # Hooks added to the group so far. @before_all_hooks = [] of -> @before_each_hooks = [] of -> @after_all_hooks = [] of -> @after_each_hooks = [] of -> @around_each_hooks = [] of Proc(Nil) -> + # Adds a new example factory or group builder to this group. def add_child(child : Child) @children << child end + # Adds a hook to run before all examples (and nested examples) in this group. def add_before_all_hook(block : ->) : Nil @before_all_hooks << block end + # Adds a hook to run before each example (and nested example) in this group. def add_before_each_hook(block : ->) : Nil @before_each_hooks << block end + # Adds a hook to run after all examples (and nested examples) in this group. def add_after_all_hook(block : ->) : Nil @after_all_hooks << block end + # Adds a hook to run after each example (and nested example) in this group. def add_after_each_hook(block : ->) : Nil @after_each_hooks << block end + # Adds a hook to run around each example (and nested example) in this group. + # The block of code will be given another proc as an argument. + # It is expected that the block will call the proc. def add_around_each_hook(block : Proc(Nil) ->) : Nil @around_each_hooks << block end + # Constructs an `ExampleHooks` instance with all the hooks defined for this group. + # This method should be only when the group is being built, + # otherwise some hooks may be missing. private def hooks ExampleHooks.new( @before_all_hooks, diff --git a/src/spectator/dsl/given_example_group_builder.cr b/src/spectator/dsl/given_example_group_builder.cr index 3346219..64c1fc4 100644 --- a/src/spectator/dsl/given_example_group_builder.cr +++ b/src/spectator/dsl/given_example_group_builder.cr @@ -1,23 +1,63 @@ require "./nested_example_group_builder" module Spectator::DSL + # Specialized example group builder for "given" groups. + # The type parameter `T` should be the type of each element in the given collection. + # This builder creates a container group with groups inside for each item in the collection. + # The hooks are only defined for the container group. + # By doing so, the hooks are defined once, are inherited, and use less memory. class GivenExampleGroupBuilder(T) < NestedExampleGroupBuilder + # Creates a new group builder. + # The value for `what` should be the text the user specified for the collection. + # The `collection` is the actual array of items to create examples for. + # + # In this code: + # ``` + # given random_integers do + # # ... + # end + # ``` + # The `what` value would be "random_integers" + # and the collection would contain the items returned by calling `random_integers`. + # + # The `symbol` is passed along to the sample values + # so that the example code can retrieve the current item from the collection. + # The symbol should be unique. def initialize(what : String, @collection : Array(T), @symbol : Symbol) super(what) end + # Builds the example group. + # A new `NestedExampleGroup` will be returned + # which can have instances of `Example` and `ExampleGroup` nested in it. + # The `parent` should be the group that contains this group. + # The `sample_values` will be given to all of the examples (and groups) nested in this group. def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup + # This creates the container for the sub-groups. + # The hooks are defined here, instead of repeating for each sub-group. NestedExampleGroup.new(@what, parent, hooks).tap do |group| + # Set the container group's children to be sub-groups for each item in the collection. group.children = @collection.map do |value| + # Create a sub-group for each item in the collection. build_sub_group(group, sample_values, value).as(ExampleComponent) end end end + # Builds a sub-group for one item in the collection. + # The `parent` should be the container group currently being built by the `#build` call. + # The `sample_values` should be the same as what was passed to the `#build` call. + # The `value` is the current item in the collection. + # The value will be added to the sample values for the sub-group, + # so it shouldn't be added prior to calling this method. private def build_sub_group(parent : ExampleGroup, sample_values : Internals::SampleValues, value : T) : NestedExampleGroup - sub_values = sample_values.add(@symbol, @symbol.to_s, value) # TODO: Use real name instead of symbol as string. + # Add the value to sample values for this sub-group. + # TODO: Use real name instead of symbol as string. + sub_values = sample_values.add(@symbol, @symbol.to_s, value) NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty).tap do |group| + # Set the sub-group's children to built versions of the children from this instance. group.children = @children.map do |child| + # Build the child and up-cast to prevent type errors. child.build(group, sub_values).as(ExampleComponent) end end diff --git a/src/spectator/dsl/nested_example_group_builder.cr b/src/spectator/dsl/nested_example_group_builder.cr index 3d6af2d..a05b428 100644 --- a/src/spectator/dsl/nested_example_group_builder.cr +++ b/src/spectator/dsl/nested_example_group_builder.cr @@ -1,11 +1,34 @@ module Spectator::DSL + # Standard example group builder. + # Creates groups of examples and nested groups. class NestedExampleGroupBuilder < ExampleGroupBuilder + # Creates a new group builder. + # The value for `what` should be the context for the group. + # + # For example, in these samples: + # ``` + # describe String do + # # ... + # context "with an empty string" do + # # ... + # end + # end + # ``` + # The value would be "String" for the describe block + # and "with an empty string" for the context block. def initialize(@what : String) end + # Builds the example group. + # A new `NestedExampleGroup` will be returned + # which can have instances of `Example` and `ExampleGroup` nested in it. + # The `parent` should be the group that contains this group. + # The `sample_values` will be given to all of the examples (and groups) nested in this group. def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup NestedExampleGroup.new(@what, parent, hooks).tap do |group| + # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| + # Build the child and up-cast to prevent type errors. child.build(group, sample_values).as(ExampleComponent) end end diff --git a/src/spectator/dsl/root_example_group_builder.cr b/src/spectator/dsl/root_example_group_builder.cr index 6321d25..87c7097 100644 --- a/src/spectator/dsl/root_example_group_builder.cr +++ b/src/spectator/dsl/root_example_group_builder.cr @@ -1,8 +1,15 @@ module Spectator::DSL + # Top-level example group builder. + # There should only be one instance of this class, + # and it should be at the top of the spec "tree". class RootExampleGroupBuilder < ExampleGroupBuilder + # Creates a `RootExampleGroup` which can have instances of `Example` and `ExampleGroup` nested in it. + # The `sample_values` will be given to all of the examples (and groups) nested in this group. def build(sample_values : Internals::SampleValues) : RootExampleGroup RootExampleGroup.new(hooks).tap do |group| + # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| + # Build the child and up-cast to prevent type errors. child.build(group, sample_values).as(ExampleComponent) end end