exemplor 2010.2.0 → 3000.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/README.md +221 -0
  2. data/Rakefile +8 -4
  3. data/TODO +13 -6
  4. data/VERSION +1 -1
  5. data/examples.rb +62 -51
  6. data/examples/assertion_failure.rb +3 -5
  7. data/examples/assertion_success.rb +2 -3
  8. data/examples/assertion_success_and_failure.rb +8 -12
  9. data/examples/assertion_success_and_info.rb +8 -11
  10. data/examples/check_parsing.rb +8 -17
  11. data/examples/checking_nil.rb +6 -5
  12. data/examples/failure_halts_execution.rb +3 -5
  13. data/examples/foobar.rb +9 -0
  14. data/examples/helpers.rb +1 -1
  15. data/examples/{with_checks.rb → multi_show.rb} +3 -3
  16. data/examples/no_checks_non_string.rb +1 -1
  17. data/examples/{check_with_disambiguation.rb → show_with_disambiguation.rb} +2 -2
  18. data/examples/{dumping_classes.rb → showing_classes.rb} +1 -1
  19. data/examples/simple_show.rb +15 -0
  20. data/examples/ten_percent_failures.rb +10 -10
  21. data/examples/with_setup.rb +5 -7
  22. data/lib/checker.rb +30 -10
  23. data/lib/environment.rb +32 -23
  24. data/lib/examples.rb +2 -1
  25. data/lib/exemplor.rb +2 -1
  26. data/lib/result_printer.rb +40 -21
  27. data/vendor/orderedhash-0.0.6/gemspec.rb +37 -0
  28. data/vendor/orderedhash-0.0.6/install.rb +213 -0
  29. data/vendor/orderedhash-0.0.6/lib/orderedautohash.rb +25 -0
  30. data/vendor/orderedhash-0.0.6/lib/orderedhash.rb +200 -0
  31. data/vendor/term-ansicolor-1.0.5/CHANGES +28 -0
  32. data/vendor/term-ansicolor-1.0.5/COPYING +340 -0
  33. data/vendor/term-ansicolor-1.0.5/README +137 -0
  34. data/vendor/term-ansicolor-1.0.5/Rakefile +88 -0
  35. data/vendor/term-ansicolor-1.0.5/VERSION +1 -0
  36. data/vendor/term-ansicolor-1.0.5/bin/cdiff +19 -0
  37. data/vendor/term-ansicolor-1.0.5/bin/decolor +12 -0
  38. data/vendor/term-ansicolor-1.0.5/examples/example.rb +90 -0
  39. data/vendor/term-ansicolor-1.0.5/install.rb +15 -0
  40. data/vendor/term-ansicolor-1.0.5/lib/term/ansicolor.rb +102 -0
  41. data/vendor/term-ansicolor-1.0.5/lib/term/ansicolor/version.rb +10 -0
  42. data/vendor/term-ansicolor-1.0.5/tests/ansicolor_test.rb +66 -0
  43. metadata +34 -41
  44. data/README +0 -152
@@ -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
- task :examples do
20
- ruby "-rubygems", "examples.rb"
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
- * add symbol to_proc and tap
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
- * test requiring exemplor does *not* trigger at exit
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
- 2010.2.0
1
+ 3000.1.0
@@ -1,74 +1,85 @@
1
- # uses the gem version, not the one being tested
2
1
  require 'exemplor'
3
2
 
4
- # slow because each test runs in a subshell
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.helpers do
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
- def run_example(name, args = nil)
9
- `ruby -rubygems -Ilib examples/#{name}.rb#{' ' + args if args}`
10
- end
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
- def extract_expected(name)
13
- File.read("examples/#{name}.rb").split('__END__').last
14
- end
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
- def expected_and_actual(example_name)
17
- [extract_expected(example_name).strip, run_example(example_name).strip]
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
- def check_output_matches_expected_for(example_name)
21
- expected_output, output = expected_and_actual(example_name)
22
- Check(output).is(expected_output)
23
- end
30
+ examples %w[
31
+ no_checks
32
+ no_checks_non_string
24
33
 
25
- end
34
+ simple_show
35
+ multi_show
36
+ show_with_disambiguation
26
37
 
27
- eg "version matches file" do
28
- version = `ruby -rubygems -Ilib -e "require 'exemplor' ; print Exemplor.version"`
29
- Check(version).is(File.read(__FILE__.sub('examples.rb','VERSION')))
30
- end
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 "errors are caught and nicely displayed" do
33
- result = YAML.load(run_example(:an_error))[0]
34
- Check(result['status']).is('error')
35
- Check(result['result']['class']).is('RuntimeError')
36
- Check(result['result']['message']).is('boom!')
37
- Check(result['result']['backtrace'][0]).is('examples/an_error.rb:4')
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 { check_output_matches_expected_for :no_checks }
41
- eg { check_output_matches_expected_for :oneliner }
42
- eg { check_output_matches_expected_for :no_checks_non_string }
43
- eg { check_output_matches_expected_for :with_checks }
44
- eg { check_output_matches_expected_for :check_with_disambiguation }
45
- eg { check_output_matches_expected_for :assertion_success }
46
- eg { check_output_matches_expected_for :assertion_failure }
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
- Check($?.exitstatus).is(10)
72
+ Assert($?.exitstatus == 10)
59
73
  end
60
74
 
61
- eg "called with --list arg" do
62
- list = YAML.load(run_example(:with_setup, '--list'))
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 "called with --l arg" do
67
- list = YAML.load(run_example(:with_setup, '--list'))
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 "called with some other arg (always interpreted as a regex)" do
72
- tests_run = YAML.load(run_example(:with_setup, 'Unmodified')).size
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
- Check(list.first).is(2)
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
- Check(list.first).is(1)
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
- Check(list.first).is(1)
6
- Check(list[1]).is(2)
7
- Check(list.last).is(1) # fail!
8
- Check(list[2]).is(3) # would be successful but we never get here
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
- result: 1
19
- - name: list[1]
18
+ - name: list[1] == 2
20
19
  status: success
21
- result: 2
22
- - name: list.last
23
- status: failure
24
- expected: 1
25
- actual: 3
20
+ - name: list.last == 1
21
+ status: failure