exemplor 2010.2.0 → 3000.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +221 -0
- data/Rakefile +8 -4
- data/TODO +13 -6
- data/VERSION +1 -1
- data/examples.rb +62 -51
- data/examples/assertion_failure.rb +3 -5
- data/examples/assertion_success.rb +2 -3
- data/examples/assertion_success_and_failure.rb +8 -12
- data/examples/assertion_success_and_info.rb +8 -11
- data/examples/check_parsing.rb +8 -17
- data/examples/checking_nil.rb +6 -5
- data/examples/failure_halts_execution.rb +3 -5
- data/examples/foobar.rb +9 -0
- data/examples/helpers.rb +1 -1
- data/examples/{with_checks.rb → multi_show.rb} +3 -3
- data/examples/no_checks_non_string.rb +1 -1
- data/examples/{check_with_disambiguation.rb → show_with_disambiguation.rb} +2 -2
- data/examples/{dumping_classes.rb → showing_classes.rb} +1 -1
- data/examples/simple_show.rb +15 -0
- data/examples/ten_percent_failures.rb +10 -10
- data/examples/with_setup.rb +5 -7
- data/lib/checker.rb +30 -10
- data/lib/environment.rb +32 -23
- data/lib/examples.rb +2 -1
- data/lib/exemplor.rb +2 -1
- data/lib/result_printer.rb +40 -21
- data/vendor/orderedhash-0.0.6/gemspec.rb +37 -0
- data/vendor/orderedhash-0.0.6/install.rb +213 -0
- data/vendor/orderedhash-0.0.6/lib/orderedautohash.rb +25 -0
- data/vendor/orderedhash-0.0.6/lib/orderedhash.rb +200 -0
- data/vendor/term-ansicolor-1.0.5/CHANGES +28 -0
- data/vendor/term-ansicolor-1.0.5/COPYING +340 -0
- data/vendor/term-ansicolor-1.0.5/README +137 -0
- data/vendor/term-ansicolor-1.0.5/Rakefile +88 -0
- data/vendor/term-ansicolor-1.0.5/VERSION +1 -0
- data/vendor/term-ansicolor-1.0.5/bin/cdiff +19 -0
- data/vendor/term-ansicolor-1.0.5/bin/decolor +12 -0
- data/vendor/term-ansicolor-1.0.5/examples/example.rb +90 -0
- data/vendor/term-ansicolor-1.0.5/install.rb +15 -0
- data/vendor/term-ansicolor-1.0.5/lib/term/ansicolor.rb +102 -0
- data/vendor/term-ansicolor-1.0.5/lib/term/ansicolor/version.rb +10 -0
- data/vendor/term-ansicolor-1.0.5/tests/ansicolor_test.rb +66 -0
- metadata +34 -41
- data/README +0 -152
data/README.md
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
Exemplor (the exemplar)
|
2
|
+
=======================
|
3
|
+
|
4
|
+
Introduction
|
5
|
+
------------
|
6
|
+
|
7
|
+
Exemplor is testing framework for high level integration tests that read like example usage scenarios. It's designed with the minimum possible vocabulary, so you can spend more time writing tests and less time learning the testing framework.
|
8
|
+
|
9
|
+
The ideal user of exemplor is a developer who is writing their program in small pieces, from the highest level of abstraction they have currently implemented.
|
10
|
+
|
11
|
+
For example lets say you were writing a command-line utility to delete all those `.DS_store` files the OS X finder generates when it's going about its business.
|
12
|
+
|
13
|
+
The approach the current crop of test frameworks encourage is to write a failing test that calls a method on an instance of a class. For example:
|
14
|
+
|
15
|
+
require 'test_framework'
|
16
|
+
require 'my_code'
|
17
|
+
|
18
|
+
context MyMainClass do
|
19
|
+
|
20
|
+
setup do
|
21
|
+
# fixture setup
|
22
|
+
end
|
23
|
+
|
24
|
+
it "deletes .DS_store files" do
|
25
|
+
MyMainClass.new(@fixture_directory).method_that_deletes_files
|
26
|
+
assert(not File(@fixture_directory + '/.DS_store').exist?)
|
27
|
+
assert(File(@fixture_directory + '/other_file.txt').exist?)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
No test framework forces the programmer to start this way. These are just the
|
33
|
+
kind of examples that come up in the documentation and the open source
|
34
|
+
projects that make use of the frameworks.
|
35
|
+
|
36
|
+
There are a number of issues with this style of testing.
|
37
|
+
|
38
|
+
1. The test file must reference:
|
39
|
+
|
40
|
+
* The main library file `my_code.rb`
|
41
|
+
* The main class `MyMainClass`
|
42
|
+
* The primary internal entry point - initialization of MyMainClass with a directory argument and then a call to `method_that_deletes_files` instance method
|
43
|
+
|
44
|
+
If any of these names change, the test must be updated accordingly.
|
45
|
+
|
46
|
+
2. There is one compulsory level of nesting in the test file. Even if each file only has one context, all tests must be nested under it.
|
47
|
+
|
48
|
+
3. If the `assert` calls fail, generally the output of the framework is not sufficient enough to determine the exact cause of failure. This is often partially solved with additional methods (or macros) such as `assert_size`, `should_exist` etc. These methods just increase the amount the user must learn to start effectively using the framework.
|
49
|
+
|
50
|
+
4. The command-line utility - the stated goal of the program - is not yet under test.
|
51
|
+
|
52
|
+
The exemplor approach would be something like:
|
53
|
+
|
54
|
+
require 'exemplor'
|
55
|
+
|
56
|
+
def run_inside directory
|
57
|
+
system "cd #{directory} && ./path/to/utility"
|
58
|
+
end
|
59
|
+
|
60
|
+
eg.setup do
|
61
|
+
# fixture setup
|
62
|
+
end
|
63
|
+
|
64
|
+
eg "deletes .DS_store files" do
|
65
|
+
run_inside @fixture_directory
|
66
|
+
Assert(not File(@fixture_directory + '/.DS_store').exist?)
|
67
|
+
Assert(File(@fixture_directory + '/other_file.txt').exist?)
|
68
|
+
end
|
69
|
+
|
70
|
+
This tests the public interface (the command-line interface) of the utility without any information about its internal implementation. All the test knows about is the path to the actual command under test.
|
71
|
+
|
72
|
+
This approach to testing allows the developer to write the first implementation without any classes, then refactor to instance methods on a class, then put that class inside a module, then move the library file into a `lib` directory, etc. All while continuing to run the tests that ensure the program works as expected.
|
73
|
+
|
74
|
+
Exemplor is not just for command-line utilities. It's for testing the parts of your application that constitute the public interface. In a web app it would be the urls. In a library, it would be the public API. Exemplor can also be used to test your underlying implementation but if you find yourself with a bunch of those tests and nothing that covers the public interface then your doing it wrong.
|
75
|
+
|
76
|
+
|
77
|
+
API Overview
|
78
|
+
------------
|
79
|
+
|
80
|
+
The api is tiny. Here it is
|
81
|
+
|
82
|
+
eg.setup do
|
83
|
+
# run before each example, set instance variables here
|
84
|
+
end
|
85
|
+
|
86
|
+
eg "an example" do
|
87
|
+
# example usage of the thing under test
|
88
|
+
Assert(expression_that.should.be(truthy))
|
89
|
+
Show(something_to_inspect)
|
90
|
+
end
|
91
|
+
|
92
|
+
eg.helpers do
|
93
|
+
# helper methods that need to run the context of an example
|
94
|
+
end
|
95
|
+
|
96
|
+
When run from a terminal, the output optimised for human readability and a high signal-to-noise ratio.
|
97
|
+
|
98
|
+
When standard out points to something other than a terminal, the output is information-rich YAML.
|
99
|
+
|
100
|
+
|
101
|
+
Writing Examples
|
102
|
+
----------------
|
103
|
+
|
104
|
+
The simplest possible example:
|
105
|
+
|
106
|
+
eg 'example without Assert() or Show() calls' do
|
107
|
+
"foo"
|
108
|
+
end
|
109
|
+
|
110
|
+
Will just print:
|
111
|
+
|
112
|
+
• example without Assert() or Show() calls: foo
|
113
|
+
|
114
|
+
This is useful for "printf driven development" where you just want to check that your code runs and does something useful. To print more than one value per example you can use the `Show()` method:
|
115
|
+
|
116
|
+
eg 'using Show()' do
|
117
|
+
list = [1, 2, 3]
|
118
|
+
Show(list.last)
|
119
|
+
list << 4
|
120
|
+
Show(list.first)
|
121
|
+
Show(list.last)
|
122
|
+
end
|
123
|
+
|
124
|
+
prints as:
|
125
|
+
|
126
|
+
∙ Showing the value of some expression
|
127
|
+
• list.first: 1
|
128
|
+
∙ using Show()
|
129
|
+
• list.last: 3
|
130
|
+
• list.first: 1
|
131
|
+
• list.last: 4
|
132
|
+
|
133
|
+
`Show()` is like a fancy `Kernel#puts`, you don't need to worry about distinguishing between different calls like you would with normal `puts`:
|
134
|
+
|
135
|
+
puts "last item: #{item.first.to_yaml}"
|
136
|
+
puts "last item: #{item.last.to_yaml}"
|
137
|
+
# etc.
|
138
|
+
|
139
|
+
because `Show()` works out the label from the source code and automatically pretty prints the value as yaml.
|
140
|
+
|
141
|
+
Exemplor has only one kind of assertion: `Assert()`. It works like `Show()` in that the label will be read from the source:
|
142
|
+
|
143
|
+
eg 'using Assert()' do
|
144
|
+
list = [1, 2, 3]
|
145
|
+
Show(list.last)
|
146
|
+
Assert(list.last == 3)
|
147
|
+
end
|
148
|
+
|
149
|
+
prints:
|
150
|
+
|
151
|
+
∙ using Assert()
|
152
|
+
• list.last: 3
|
153
|
+
✓ list.last == 3
|
154
|
+
|
155
|
+
If the example contains no `Show()` calls and all the asserts are successful then the entire example is considered successful:
|
156
|
+
|
157
|
+
eg 'using Assert()' do
|
158
|
+
list = [1, 2, 3]
|
159
|
+
Assert(list.first == 1)
|
160
|
+
Assert(list.last == 3)
|
161
|
+
end
|
162
|
+
|
163
|
+
prints:
|
164
|
+
|
165
|
+
✓ using Assert()
|
166
|
+
|
167
|
+
If an assertion fails then the name of the test is printed with an ✗ next to it:
|
168
|
+
|
169
|
+
eg 'The second Assert() will fail' do
|
170
|
+
list = [1, 2, 3]
|
171
|
+
Assert(list.first == 1)
|
172
|
+
Assert(list.last == 1)
|
173
|
+
end
|
174
|
+
|
175
|
+
prints:
|
176
|
+
|
177
|
+
✗ The second Assert() will fail
|
178
|
+
✓ list.first == 1
|
179
|
+
✗ list.last == 1
|
180
|
+
|
181
|
+
Nothing fancy, no "expected" and "actual" values are printed, if you want to inspect those you can just add a `Show()` call before the assert.
|
182
|
+
|
183
|
+
|
184
|
+
Running Examples
|
185
|
+
----------------
|
186
|
+
|
187
|
+
Run the example file through ruby
|
188
|
+
|
189
|
+
$> ruby examples.rb
|
190
|
+
|
191
|
+
To run only examples that match the regex "location | setting/x"
|
192
|
+
|
193
|
+
$> ruby examples.rb "location | setting/x"
|
194
|
+
|
195
|
+
Running with `--list` or `-l` lists all examples:
|
196
|
+
|
197
|
+
$> ruby examples.rb -l
|
198
|
+
- errors are caught and nicely displayed
|
199
|
+
- check_output_matches_expected_for :no_checks
|
200
|
+
- check_output_matches_expected_for :oneliner
|
201
|
+
- check_output_matches_expected_for :no_checks_non_string
|
202
|
+
- check_output_matches_expected_for :with_checks
|
203
|
+
- check_output_matches_expected_for :check_with_disambiguation
|
204
|
+
- check_output_matches_expected_for :assertion_success
|
205
|
+
- check_output_matches_expected_for :assertion_failure
|
206
|
+
- check_output_matches_expected_for :assertion_success_and_failure
|
207
|
+
- check_output_matches_expected_for :helpers
|
208
|
+
- check_output_matches_expected_for :with_setup
|
209
|
+
- called with --list arg
|
210
|
+
- called with --l arg
|
211
|
+
- called with some other arg (always interpreted as a regex)
|
212
|
+
|
213
|
+
|
214
|
+
Thanks
|
215
|
+
------
|
216
|
+
|
217
|
+
Exemplor was inspired by [testy](http://github.com/ahoward/testy).
|
218
|
+
|
219
|
+
What really kicked me over the line to this style of testing and eventually lead to writing exemplor was Yehuda Katz's "Writing Code That Doesn't Suck" presentation at RubyConf 2008. I wasn't there but luckily an excellent video of the presentation is available online[1]. If none of this readme made any sense to you or you have additional questions/concerns I strongly recommend watching that presentation.
|
220
|
+
|
221
|
+
[1] http://rubyconf2008.confreaks.com/writing-code-that-doesnt-suck.html
|
data/Rakefile
CHANGED
@@ -8,16 +8,20 @@ begin
|
|
8
8
|
gs.summary = "A light-weight, low-fi way to provide executable usage examples of your code."
|
9
9
|
gs.email = "myles@myles.id.au"
|
10
10
|
gs.authors = ["Myles Byrne"]
|
11
|
-
gs.add_dependency('orderedhash', '>= 0.0.6')
|
12
|
-
gs.add_dependency('term-ansicolor', '>= 1.0.3')
|
13
11
|
end
|
14
12
|
Jeweler::GemcutterTasks.new
|
15
13
|
rescue LoadError
|
16
14
|
puts "Install jeweler to build gem"
|
17
15
|
end
|
18
16
|
|
19
|
-
|
20
|
-
|
17
|
+
desc "runs the examples with the rubygems version of exemplor (you must gem install the gem for this to work)"
|
18
|
+
task :examples, [:filter] do |_,args|
|
19
|
+
ruby "-rubygems", "examples.rb", (args.filter || '')
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "runs the examples with the development version (i.e. the one in this dir) of exemplor"
|
23
|
+
task :dev, [:filter] do |_,args|
|
24
|
+
ruby '-rubygems', '-I', 'lib', 'examples.rb', (args.filter || '')
|
21
25
|
end
|
22
26
|
|
23
27
|
task :test => :examples
|
data/TODO
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
|
2
|
-
* exemplor executable that can run multiple example files
|
3
|
-
* TextMate bundle that does nice cmd+r output
|
4
|
-
* smarter macros for examples.rb - this could be clearer
|
5
|
-
* discuss example grouping in doc (philosophy)
|
1
|
+
Stuff that's on my radar:
|
6
2
|
|
7
|
-
*
|
3
|
+
* Halt()
|
4
|
+
* Run Assert() and Show() checks should appear before the error report
|
5
|
+
* --version arg
|
6
|
+
|
7
|
+
* Documentation on how to use with rack
|
8
|
+
* Documentation on why the one-context-per-file approach is used
|
9
|
+
|
10
|
+
* An exemplor executable that can run multiple example files
|
11
|
+
|
12
|
+
* test stderr capturing
|
13
|
+
* adding symbol to_proc and tap to ext.rb
|
14
|
+
* test requiring an examples file from another file does *not* trigger at_exit
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3000.1.0
|
data/examples.rb
CHANGED
@@ -1,74 +1,85 @@
|
|
1
|
-
# uses the gem version, not the one being tested
|
2
1
|
require 'exemplor'
|
3
2
|
|
4
|
-
#
|
3
|
+
# Each test runs in a subshell, exemplor is tested with exemplor but from
|
4
|
+
# a version running in a different process. Exemplor hates unit tests.
|
5
5
|
|
6
|
-
eg.
|
6
|
+
eg "Exemplor.version comes from the version file" do
|
7
|
+
version = `ruby -Ilib -e "require 'exemplor' ; print Exemplor.version"`
|
8
|
+
version_from_file = File.read(__FILE__.sub('examples.rb','VERSION'))
|
9
|
+
Assert(version == version_from_file)
|
10
|
+
end
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
# runs an example file (in /examples) using the development version of exemplor
|
13
|
+
def run_example(name, args = nil)
|
14
|
+
`ruby -Ilib examples/#{name}.rb#{' ' + args if args}`
|
15
|
+
end
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
# pulls out text after the __END__ in an example file
|
18
|
+
def expected_output_for name
|
19
|
+
File.read("examples/#{name}.rb").split('__END__').last.lstrip + "\n"
|
20
|
+
end
|
15
21
|
|
16
|
-
|
17
|
-
|
22
|
+
# a macro that runs an example file and then asserts that the output matches
|
23
|
+
# the expected output which is specified after the __END__ in that same file
|
24
|
+
def examples filenames
|
25
|
+
filenames.each do |file|
|
26
|
+
eg("#{file}.rb") { Assert(run_example(file) == expected_output_for(file)) }
|
18
27
|
end
|
28
|
+
end
|
19
29
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
30
|
+
examples %w[
|
31
|
+
no_checks
|
32
|
+
no_checks_non_string
|
24
33
|
|
25
|
-
|
34
|
+
simple_show
|
35
|
+
multi_show
|
36
|
+
show_with_disambiguation
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
38
|
+
assertion_success
|
39
|
+
assertion_failure
|
40
|
+
assertion_success_and_failure
|
41
|
+
assertion_success_and_info
|
42
|
+
failure_halts_execution
|
43
|
+
|
44
|
+
helpers
|
45
|
+
with_setup
|
46
|
+
checking_nil
|
47
|
+
showing_classes
|
48
|
+
check_parsing
|
49
|
+
]
|
50
|
+
|
51
|
+
# I never use this guy, candidate for removal
|
52
|
+
examples %w[oneliner]
|
31
53
|
|
32
|
-
eg
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
54
|
+
eg.helpers do
|
55
|
+
# Exemplor outputs valid yaml, for some of our assertions it's easier to use
|
56
|
+
# the parsed structure
|
57
|
+
def parse_run *args
|
58
|
+
YAML.load(run_example(*args))
|
59
|
+
end
|
38
60
|
end
|
39
61
|
|
40
|
-
eg
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
eg { check_output_matches_expected_for :assertion_success_and_failure }
|
48
|
-
eg { check_output_matches_expected_for :assertion_success_and_info }
|
49
|
-
eg { check_output_matches_expected_for :failure_halts_execution }
|
50
|
-
eg { check_output_matches_expected_for :helpers }
|
51
|
-
eg { check_output_matches_expected_for :with_setup }
|
52
|
-
eg { check_output_matches_expected_for :checking_nil }
|
53
|
-
eg { check_output_matches_expected_for :dumping_classes }
|
54
|
-
eg { check_output_matches_expected_for :check_parsing }
|
62
|
+
eg "errors are caught and backtraces shown" do
|
63
|
+
result = parse_run(:an_error)[0]
|
64
|
+
Assert(result['status'] == 'error')
|
65
|
+
Assert(result['result']['class'] == 'RuntimeError')
|
66
|
+
Assert(result['result']['message'] == 'boom!')
|
67
|
+
Assert(result['result']['backtrace'][0] == 'examples/an_error.rb:4')
|
68
|
+
end
|
55
69
|
|
56
70
|
eg "exit status is percent of issues that failed or errored" do
|
57
71
|
run_example :ten_percent_failures
|
58
|
-
|
72
|
+
Assert($?.exitstatus == 10)
|
59
73
|
end
|
60
74
|
|
61
|
-
eg "
|
62
|
-
|
63
|
-
Check(list).is(["Modified env", "Unmodified env"])
|
75
|
+
eg "--list shows all the example names in the file" do
|
76
|
+
Assert(parse_run(:foobar, '--list') == ["foo", "bar"])
|
64
77
|
end
|
65
78
|
|
66
|
-
eg "
|
67
|
-
|
68
|
-
Check(list).is(["Modified env", "Unmodified env"])
|
79
|
+
eg "-l is the same as --list" do
|
80
|
+
Assert(run_example(:foobar, '-l') == run_example(:foobar, '--list'))
|
69
81
|
end
|
70
82
|
|
71
|
-
eg "
|
72
|
-
|
73
|
-
Check(tests_run).is(1)
|
83
|
+
eg "any other arg is intepreted as a regex and the examples that match it are run" do
|
84
|
+
Assert(parse_run(:foobar, 'foo').size == 1)
|
74
85
|
end
|
@@ -2,7 +2,7 @@ require 'exemplor'
|
|
2
2
|
|
3
3
|
eg 'Assertion failure' do
|
4
4
|
list = [1, 2, 3]
|
5
|
-
|
5
|
+
Assert(list.first == 2)
|
6
6
|
end
|
7
7
|
|
8
8
|
__END__
|
@@ -10,7 +10,5 @@ __END__
|
|
10
10
|
- name: Assertion failure
|
11
11
|
status: failure
|
12
12
|
result:
|
13
|
-
- name: list.first
|
14
|
-
status: failure
|
15
|
-
expected: 2
|
16
|
-
actual: 1
|
13
|
+
- name: list.first == 2
|
14
|
+
status: failure
|
@@ -2,7 +2,7 @@ require 'exemplor'
|
|
2
2
|
|
3
3
|
eg 'Asserting first is first' do
|
4
4
|
list = [1, 2, 3]
|
5
|
-
|
5
|
+
Assert(list.first)
|
6
6
|
end
|
7
7
|
|
8
8
|
__END__
|
@@ -11,5 +11,4 @@ __END__
|
|
11
11
|
status: success
|
12
12
|
result:
|
13
13
|
- name: list.first
|
14
|
-
status: success
|
15
|
-
result: 1
|
14
|
+
status: success
|
@@ -2,10 +2,10 @@ require 'exemplor'
|
|
2
2
|
|
3
3
|
eg 'Some successes, then a fail' do
|
4
4
|
list = [1, 2, 3]
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
Assert(list.first == 1)
|
6
|
+
Assert(list[1] == 2)
|
7
|
+
Assert(list.last == 1) # fail!
|
8
|
+
Assert(list[2] == 3) # would be successful but we never get here
|
9
9
|
end
|
10
10
|
|
11
11
|
__END__
|
@@ -13,13 +13,9 @@ __END__
|
|
13
13
|
- name: Some successes, then a fail
|
14
14
|
status: failure
|
15
15
|
result:
|
16
|
-
- name: list.first
|
16
|
+
- name: list.first == 1
|
17
17
|
status: success
|
18
|
-
|
19
|
-
- name: list[1]
|
18
|
+
- name: list[1] == 2
|
20
19
|
status: success
|
21
|
-
|
22
|
-
|
23
|
-
status: failure
|
24
|
-
expected: 1
|
25
|
-
actual: 3
|
20
|
+
- name: list.last == 1
|
21
|
+
status: failure
|