scope 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. data/lib/scope.rb +146 -0
  2. data/scope.gemspec +21 -0
  3. metadata +81 -0
data/lib/scope.rb ADDED
@@ -0,0 +1,146 @@
1
+ require "minitest/unit"
2
+
3
+ module Scope
4
+ # A test case class which provides nested contexts. Subclasses will have the "setup", "teardown", and
5
+ # "should" methods available as class methods.
6
+ class TestCase < MiniTest::Unit::TestCase
7
+ # A map of test name => Context.
8
+ def self.context_for_test() @context_for_test end
9
+
10
+ def self.inherited(subclass)
11
+ # Calling Unit::TestCase's inherited() method is important, as that's how it registers test suites.
12
+ super
13
+
14
+ subclass.instance_eval do
15
+ # Pretend the whole test is wrapped in a context, so we can always code as if tests are in contexts.
16
+ @contexts = [Context.new("")]
17
+ @context_for_test = {}
18
+
19
+ # The tests defined in this test case. MiniTest::Unit::TestCase sorts these methods randomly or
20
+ # alphabetically. Let's run them in the order they were defined, as that's least surprising.
21
+ def test_methods()
22
+ tests = []
23
+ stack = [@contexts.first]
24
+ until stack.empty? do
25
+ item = stack.pop
26
+ stack += item.tests_and_subcontexts.reverse if item.is_a?(Context)
27
+ tests << item if item.is_a?(String)
28
+ end
29
+ tests
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.context(name, &block)
35
+ parent = @contexts.last
36
+ new_context = Context.new(name, parent)
37
+ parent.tests_and_subcontexts << new_context
38
+ @contexts << new_context
39
+ block.call
40
+ @contexts.pop
41
+ end
42
+
43
+ def self.should(name, &block)
44
+ # When focus_enabled is true, we'll only be running the next should() block that gets defined.
45
+ if @focus_enabled
46
+ return unless @focus_next_test
47
+ @focus_next_test = false
48
+ end
49
+
50
+ context_name = @contexts[1..-1].map(&:name).join(" ")
51
+ context_name += " " unless context_name.empty?
52
+ test_method_name = "#{context_name}should #{name}"
53
+ define_method test_method_name, block
54
+ @contexts.last.tests_and_subcontexts << test_method_name
55
+ @context_for_test[test_method_name] = @contexts.last
56
+ end
57
+
58
+ def self.setup(&block) @contexts.last.add_setup(&block) end
59
+ def self.teardown(&block) @contexts.last.add_teardown(&block) end
60
+
61
+ # setup_once blocks are run just once for a context, and not on a per-test basis. They are useful
62
+ # for integration tests with costly setup.
63
+ def self.setup_once(&block) @contexts.last.add_setup_once(&block) end
64
+ def self.teardown_once(&block) @contexts.last.add_teardown_once(&block) end
65
+
66
+ # "Focuses" the next test that's defined after this method is called, ensuring that only that test is run.
67
+ def self.focus
68
+ # Since we're focusing only the next test, remove any tests which were already defined.
69
+ context_for_test.values.uniq.each do |context|
70
+ context.tests_and_subcontexts.reject! { |test| test.is_a?(String) }
71
+ end
72
+ @focus_enabled = true
73
+ @focus_next_test = true
74
+ end
75
+
76
+ # run() is called by the MiniTest framework. This TestCase class is instantiated once per test method
77
+ # defined, and then run() is called on each test case instance.
78
+ def run(test_runner)
79
+ test_name = self.__name__
80
+ context = self.class.context_for_test[test_name]
81
+ result = nil
82
+ # Unit::TestCase's implementation of run() invokes the test method (test_name) with exception handling.
83
+ context.run_setup_and_teardown(test_name) { result = super }
84
+ result
85
+ end
86
+ end
87
+
88
+ # A context keeps track of the tests defined inside of it as well as its setup and teardown blocks.
89
+ class Context
90
+ attr_reader :name, :parent_context
91
+ # We keep both tests and subcontexts in the same array because we need to know what the very last thing
92
+ # to execute inside of this context is, for the purpose of calling teardown_once at the correct time.
93
+ attr_accessor :tests_and_subcontexts
94
+
95
+ def initialize(name, parent_context = nil)
96
+ @name = name
97
+ @parent_context = parent_context
98
+ self.tests_and_subcontexts = []
99
+ end
100
+
101
+ # Runs the setup work for this context and any parent contexts, yields to the block (which should invoke
102
+ # the actual test method), and then completes the teardown work.
103
+ def run_setup_and_teardown(test_name)
104
+ contexts = ([self] + self.ancestor_contexts).reverse
105
+ contexts.each(&:setup_once)
106
+ contexts.each(&:setup)
107
+ yield
108
+ contexts.reverse!
109
+ contexts.each(&:teardown)
110
+
111
+ # If this is the last context being run in any parent contexts, run their teardown_once blocks.
112
+ if tests_and_subcontexts.last == test_name
113
+ self.teardown_once
114
+ descendant_context = nil
115
+ contexts.each do |ancestor|
116
+ break unless ancestor.tests_and_subcontexts.last == descendant_context
117
+ ancestor.teardown_once
118
+ descendant_context = ancestor
119
+ end
120
+ end
121
+ end
122
+
123
+ def ancestor_contexts
124
+ ancestors = []
125
+ parent = self
126
+ ancestors << parent while (parent = parent.parent_context)
127
+ ancestors
128
+ end
129
+
130
+ def add_setup(&block) @setup = block end
131
+ def add_teardown(&block) @teardown = block end
132
+ def add_setup_once(&block) @setup_once = run_only_once(&block) end
133
+ def add_teardown_once(&block) @teardown_once = run_only_once(&block) end
134
+
135
+ def setup() @setup.call if @setup end
136
+ def teardown() @teardown.call if @teardown end
137
+ def setup_once() @setup_once.call if @setup_once end
138
+ def teardown_once() @teardown_once.call if @teardown_once end
139
+
140
+ private
141
+ def run_only_once(&block)
142
+ has_run = false
143
+ Proc.new { block.call unless has_run; has_run = true }
144
+ end
145
+ end
146
+ end
data/scope.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "scope"
3
+ s.version = "0.1.1"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">=0") if s.respond_to? :required_rubygems_version=
6
+ s.specification_version = 2 if s.respond_to? :specification_version=
7
+
8
+ s.author = "Phil Crosby"
9
+ s.email = "phil.crosby@gmail.com"
10
+
11
+ s.description = "Concise unit testing in the spirit of Shoulda"
12
+ s.summary = "Concise unit testing in the spirit of Shoulda"
13
+ s.homepage = "http://github.com/ooyala/scope"
14
+ s.rubyforge_project = "scope"
15
+
16
+ s.files = %w(
17
+ scope.gemspec
18
+ lib/scope.rb
19
+ )
20
+ s.add_dependency("minitest")
21
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scope
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Phil Crosby
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-05-22 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: minitest
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Concise unit testing in the spirit of Shoulda
36
+ email: phil.crosby@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - scope.gemspec
45
+ - lib/scope.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/ooyala/scope
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options: []
52
+
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ hash: 3
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ requirements: []
74
+
75
+ rubyforge_project: scope
76
+ rubygems_version: 1.6.2
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: Concise unit testing in the spirit of Shoulda
80
+ test_files: []
81
+