protest 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ doc
2
+ dist
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2009 Nicolas Sanguinetti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,190 @@
1
+ = Protest, the simplicity rebel test framework
2
+
3
+ require "protest"
4
+
5
+ Protest.context("A user") do
6
+ setup do
7
+ @user = User.new(:name => "John Doe", :email => "john@example.org")
8
+ end
9
+
10
+ test "has a name" do
11
+ assert @user.name == "John Doe"
12
+ end
13
+
14
+ test "has an email" do
15
+ assert @user.email == "john@example.org"
16
+ end
17
+ end
18
+
19
+ Protest is a small, simple, and easy-to-extend testing framework for ruby. It
20
+ was written as a replacement for Test::Unit, given how awful its code is, and
21
+ how difficult it is to extend in order to add new features.
22
+
23
+ I believe in minimalistic software, which is easily understood, easy to test,
24
+ and specially, easy to extend for third parties. That's where I'm aiming with
25
+ Protest.
26
+
27
+ == Get it
28
+
29
+ gem install protest
30
+
31
+ Or
32
+
33
+ rip install git://github.com/foca/protest.git
34
+
35
+ == Assertions
36
+
37
+ You probably wonder why the above example doesn't have any assertion other than
38
+ +assert+. Where's +assert_equal+, right? Well, the idea is to keep it slim. If
39
+ you want to use more assertions, you're free to define your own. You can also
40
+ use all the assertions provided by Test::Unit, by just including them:
41
+
42
+ require "test/unit/assertions"
43
+
44
+ class Protest::TestCase
45
+ include Test::Unit::Assertions
46
+ end
47
+
48
+ You could also define rspec-like matchers if you like that. See +matchers.rb+ in
49
+ the examples directory for an example.
50
+
51
+ == Setup and teardown
52
+
53
+ If you need to run code before or after each test, declare a +setup+ or
54
+ +teardown+ block (respectively.)
55
+
56
+ Protest.context("A user") do
57
+ setup do # this runs before each test
58
+ @user = User.create(:name => "John")
59
+ end
60
+
61
+ teardown do # this runs after each test
62
+ @user.destroy
63
+ end
64
+ end
65
+
66
+ +setup+ and +teardown+ blocks are evaluated in the same context as your test,
67
+ which means any instance variables defined in any of them are available in the
68
+ rest.
69
+
70
+ You can also use +global_setup+ and +global_teardown+ to run code only once per
71
+ test case. +global_setup+ blocks will run once before the first test is run, and
72
+ +global_teardown+ will run after all the tests have been run.
73
+
74
+ These methods, however, are dangerous, and should be used with caution, as
75
+ they might introduce dependencies between your tests if you don't write
76
+ your tests properly. Make sure that any state modified by code run in a
77
+ +global_setup+ or +global_teardown+ isn't changed in any of your tests.
78
+
79
+ Also, you should be aware that the code of +global_setup+ and +global_teardown+
80
+ blocks isn't evaluated in the same context as your tests and normal
81
+ +setup+/+teardown+ blocks are, so you can't share instance variables between
82
+ them.
83
+
84
+ == Nested contexts
85
+
86
+ Break down your test into logical chunks with nested contexts:
87
+
88
+ Protest.context("A user") do
89
+ setup do
90
+ @user = User.make
91
+ end
92
+
93
+ context "when validating" do
94
+ test "validates name" do
95
+ @user.name = nil
96
+ assert !@user.valid?
97
+ end
98
+
99
+ # etc, etc
100
+ end
101
+
102
+ context "doing something else" do
103
+ # your get the idea
104
+ end
105
+ end
106
+
107
+ Any +setup+ or +teardown+ blocks you defined in a context will run in that
108
+ context and in _any_ other context nested in it.
109
+
110
+ == Pending tests
111
+
112
+ There are two ways of marking a test as pending. You can declare a test with no
113
+ body:
114
+
115
+ Protest.context("Some tests") do
116
+ test "this test will be marked as pending"
117
+
118
+ test "this tests is also pending"
119
+
120
+ test "this test isn't pending" do
121
+ assert true
122
+ end
123
+ end
124
+
125
+ Or you can call the +pending+ method from inside your test.
126
+
127
+ Protest.context("Some tests") do
128
+ test "this test is pending" do
129
+ pending "oops, this doesn't work"
130
+ assert false
131
+ end
132
+ end
133
+
134
+ == Reports
135
+
136
+ Protest can report the output of a test suite in many ways. The library ships
137
+ with a +:progress+ report, and a +:documentation+ report, +:progress+ being the
138
+ default.
139
+
140
+ === Progress report
141
+
142
+ Use this report by calling <tt>Protest.report_with(:progress)</tt> (this isn't
143
+ needed though, since this is the default report.)
144
+
145
+ The progress report will output the "classic" Test::Unit output of periods for
146
+ passing tests, "F" for failing assertions, "E" for unrescued exceptions, and
147
+ "P" for pending tests, in full color.
148
+
149
+ === Documentation report
150
+
151
+ Use this report by calling <tt>Protest.report_with(:documentation)</tt>
152
+
153
+ For each testcase in your suite, this will output the description of the test
154
+ case (whatever you provide TestCase.context), followed by the name of each test
155
+ in that context, one per line. For example:
156
+
157
+ Protest.context "A user" do
158
+ test "has a name"
159
+ test "has an email"
160
+
161
+ context "validations" do
162
+ test "ensure the email can't be blank"
163
+ end
164
+ end
165
+
166
+ Will output, when run with the +:documentation+ report:
167
+
168
+ A user
169
+ - has a name (Not Yet Implemented)
170
+ - has an email (Not Yet Implemented)
171
+
172
+ A user validations
173
+ - ensure the email can't be blank (Not Yet Implemented)
174
+
175
+ (The 'Not Yet Implemented' messages are because the tests have no body. See
176
+ "Pending tests", above.)
177
+
178
+ This is similar to the specdoc runner in rspec[http://rspec.info].
179
+
180
+ === Defining your own reports
181
+
182
+ This is really, really easy. All you need to do is subclass Report, and
183
+ register your subclass by calling +Protest.add_report+. See the
184
+ documentation for details, or take a look at the source code for
185
+ Protest::Reports::Progress and Protest::Reports::Documentation.
186
+
187
+ == Legal
188
+
189
+ Author:: Nicolás Sanguinetti — http://nicolassanguinetti.info
190
+ License:: MIT (see bundled LICENSE file for more info)
@@ -0,0 +1,24 @@
1
+ begin
2
+ require "hanna/rdoctask"
3
+ rescue LoadError
4
+ require "rake/rdoctask"
5
+ end
6
+
7
+ require "rake/testtask"
8
+
9
+ Rake::RDocTask.new do |rd|
10
+ rd.main = "README.rdoc"
11
+ rd.title = "API Documentation for Protest"
12
+ rd.rdoc_files.include("README.rdoc", "LICENSE", "lib/**/*.rb")
13
+ rd.rdoc_dir = "doc"
14
+ end
15
+
16
+ begin
17
+ require "mg"
18
+ MG.new("protest.gemspec")
19
+ rescue LoadError
20
+ end
21
+
22
+ Rake::TestTask.new
23
+
24
+ task :default => :test
@@ -0,0 +1,94 @@
1
+ module Protest
2
+ # Exception raised when an assertion fails. See TestCase#assert
3
+ class AssertionFailed < StandardError; end
4
+
5
+ # Exception raised to mark a test as pending. See TestCase#pending
6
+ class Pending < StandardError; end
7
+
8
+ # Register a new Report. This will make your report available to Protest,
9
+ # allowing you to run your tests through this report. For example
10
+ #
11
+ # module Protest
12
+ # class Reports::MyAwesomeReport < Report
13
+ # end
14
+ #
15
+ # add_report :awesomesauce, MyAwesomeReport
16
+ # end
17
+ #
18
+ # See Protest.report_with to see how to select which report will be used.
19
+ def self.add_report(name, report)
20
+ available_reports[name] = report
21
+ end
22
+
23
+ # Register a test case to be run with Protest. This is done automatically
24
+ # whenever you subclass Protest::TestCase, so you probably shouldn't pay
25
+ # much attention to this method.
26
+ def self.add_test_case(test_case)
27
+ available_test_cases << test_case
28
+ end
29
+
30
+ # Set to +false+ to avoid running tests +at_exit+. Default is +true+.
31
+ def self.autorun=(flag)
32
+ @autorun = flag
33
+ end
34
+
35
+ # Checks to see if tests should be run +at_exit+ or not. Default is +true+.
36
+ # See Protest.autorun=
37
+ def self.autorun?
38
+ !!@autorun
39
+ end
40
+
41
+ # Run all registered test cases through the selected report. You can pass
42
+ # arguments to the Report constructor here.
43
+ #
44
+ # See Protest.add_test_case and Protest.report_with
45
+ def self.run_all_tests!(*report_args)
46
+ Runner.new(@report).run(*available_test_cases)
47
+ end
48
+
49
+ # Select the name of the Report to use when running tests. See
50
+ # Protest.add_report for more information on registering a report.
51
+ #
52
+ # Any extra arguments will be forwarded to the report's #initialize method.
53
+ #
54
+ # The default report is Protest::Reports::Progress
55
+ def self.report_with(name, *report_args)
56
+ @report = report(name, *report_args)
57
+ end
58
+
59
+ # Load a report by name, initializing it with the extra arguments provided.
60
+ # If the given +name+ doesn't match a report registered via
61
+ # Protest.add_report then the method will raise IndexError.
62
+ def self.report(name, *report_args)
63
+ available_reports.fetch(name).new(*report_args)
64
+ end
65
+
66
+ def self.available_test_cases
67
+ @test_cases ||= []
68
+ end
69
+ private_class_method :available_test_cases
70
+
71
+ def self.available_reports
72
+ @available_reports ||= {}
73
+ end
74
+ private_class_method :available_reports
75
+ end
76
+
77
+ require "protest/utils"
78
+ require "protest/utils/backtrace_filter"
79
+ require "protest/utils/summaries"
80
+ require "protest/utils/colorful_output"
81
+ require "protest/test_case"
82
+ require "protest/tests"
83
+ require "protest/runner"
84
+ require "protest/report"
85
+ require "protest/reports"
86
+ require "protest/reports/progress"
87
+ require "protest/reports/documentation"
88
+
89
+ Protest.autorun = true
90
+ Protest.report_with(:progress)
91
+
92
+ at_exit do
93
+ Protest.run_all_tests! if Protest.autorun?
94
+ end
@@ -0,0 +1,62 @@
1
+ require "protest"
2
+ require "test/unit/assertions"
3
+ require "action_controller/test_case"
4
+ require "webrat"
5
+
6
+ module Protest
7
+ module Rails
8
+ # Wrap all tests in a database transaction.
9
+ #
10
+ # TODO: make this optional somehow (yet enabled by default) so users of
11
+ # other ORMs don't run into problems.
12
+ module TransactionalTests
13
+ def run(*args, &block)
14
+ ActiveRecord::Base.connection.transaction do
15
+ super(*args, &block)
16
+ raise ActiveRecord::Rollback
17
+ end
18
+ end
19
+ end
20
+
21
+ # You should inherit from this TestCase in order to get rails' helpers
22
+ # loaded into Protest. These include all the assertions bundled with rails
23
+ # and your tests being wrapped in a transaction.
24
+ class TestCase < ::Protest::TestCase
25
+ include ::Test::Unit::Assertions
26
+ include ActiveSupport::Testing::Assertions
27
+ include TransactionalTests
28
+ end
29
+
30
+ class RequestTest < TestCase #:nodoc:
31
+ %w(response selector tag dom routing model).each do |kind|
32
+ include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
33
+ end
34
+ end
35
+
36
+ # Make your integration tests inherit from this class, which bundles the
37
+ # integration runner included with rails, and webrat's test methods. You
38
+ # should use webrat for integration tests. Really.
39
+ class IntegrationTest < RequestTest
40
+ include ActionController::Integration::Runner
41
+ include Webrat::Methods
42
+ end
43
+ end
44
+
45
+ # The preferred way to declare a context (top level) is to use
46
+ # +Protest.describe+ or +Protest.context+, which will ensure you're using
47
+ # rails adapter with the helpers you need.
48
+ def self.context(description, &block)
49
+ Rails::TestCase.context(description, &block)
50
+ end
51
+
52
+ # Use +Protest.story+ to declare an integration test for your rails app. Note
53
+ # that the files should still be called 'test/integration/foo_test.rb' if you
54
+ # want the 'test:integration' rake task to pick them up.
55
+ def self.story(description, &block)
56
+ Rails::IntegrationTest.story(description, &block)
57
+ end
58
+
59
+ class << self
60
+ alias_method :describe, :context
61
+ end
62
+ end
@@ -0,0 +1,113 @@
1
+ module Protest
2
+ class Report
3
+ # Define an event handler for your report. The different events fired in a
4
+ # report's life cycle are:
5
+ #
6
+ # :start:: Fired by the runner when starting the whole test suite.
7
+ # :enter:: Fired by the runner when starting a particular test case. It
8
+ # will get the test case as an argument.
9
+ # :test:: Fired by a test before it starts running. It will get the
10
+ # instance of TestCase for the given test as an argument.
11
+ # :assertion:: Fired by a test each time an assertion is run.
12
+ # :pass:: Fired by a test after it runs successfully without errors.
13
+ # It will get an instance of PassedTest as an argument.
14
+ # :pending:: Fired by a test which doesn't provide a test block or which
15
+ # calls TestCase#pending. It will get an instance of
16
+ # PendingTest as an argument.
17
+ # :failure:: Fired by a test in which an assertion failed. It will get an
18
+ # instance of FailedTest as an argument.
19
+ # :error:: Fired by a test where an uncaught exception was found. It
20
+ # will get an instance of ErroredTest as an argument.
21
+ # :exit:: Fired by the runner each time a test case finishes. It will
22
+ # take the test case as an argument.
23
+ # :end:: Fired by the runner at the end of the whole test suite.
24
+ #
25
+ # The event handler will receive the report as a first argument, plus any
26
+ # arguments documented above (depending on the event). It will also ensure
27
+ # that any handler for the same event declared on an ancestor class is run.
28
+ def self.on(event, &block)
29
+ define_method(:"on_#{event}") do |*args|
30
+ begin
31
+ super(*args)
32
+ rescue NoMethodError
33
+ end
34
+
35
+ block.call(self, *args)
36
+ end
37
+ end
38
+
39
+ on :start do |report|
40
+ report.instance_eval { @started_at = Time.now }
41
+ end
42
+
43
+ on :pass do |report, passed_test|
44
+ report.passes << passed_test
45
+ end
46
+
47
+ on :pending do |report, pending_test|
48
+ report.pendings << pending_test
49
+ end
50
+
51
+ on :failure do |report, failed_test|
52
+ report.failures << failed_test
53
+ report.failures_and_errors << failed_test
54
+ end
55
+
56
+ on :error do |report, errored_test|
57
+ report.errors << errored_test
58
+ report.failures_and_errors << errored_test
59
+ end
60
+
61
+ on :assertion do |report|
62
+ report.add_assertion
63
+ end
64
+
65
+ # List all the tests (as PendingTest instances) that were pending.
66
+ def pendings
67
+ @pendings ||= []
68
+ end
69
+
70
+ # List all the tests (as PassedTest instances) that passed.
71
+ def passes
72
+ @passes ||= []
73
+ end
74
+
75
+ # List all the tests (as FailedTest instances) that failed an assertion.
76
+ def failures
77
+ @failures ||= []
78
+ end
79
+
80
+ # List all the tests (as ErroredTest instances) that raised an unrescued
81
+ # exception.
82
+ def errors
83
+ @errors ||= []
84
+ end
85
+
86
+ # Aggregated and ordered list of tests that either failed an assertion or
87
+ # raised an unrescued exception. Useful for displaying back to the user.
88
+ def failures_and_errors
89
+ @failures_and_errors ||= []
90
+ end
91
+
92
+ # Log an assertion was run (whether it succeeded or failed.)
93
+ def add_assertion
94
+ @assertions ||= 0
95
+ @assertions += 1
96
+ end
97
+
98
+ # Number of assertions run during the report.
99
+ def assertions
100
+ @assertions || 0
101
+ end
102
+
103
+ # Amount ot tests run (whether passed, pending, failed, or errored.)
104
+ def total_tests
105
+ passes.size + failures.size + errors.size + pendings.size
106
+ end
107
+
108
+ # Seconds taken since the test suite started running
109
+ def time_elapsed
110
+ Time.now - @started_at
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,2 @@
1
+ module Protest::Reports # :nodoc:
2
+ end
@@ -0,0 +1,77 @@
1
+ module Protest
2
+ # For each testcase in your suite, this will output the description of the test
3
+ # case (whatever you provide TestCase.context), followed by the name of each test
4
+ # in that context, one per line. For example:
5
+ #
6
+ # Protest.context "A user" do
7
+ # test "has a name" do
8
+ # ...
9
+ # end
10
+ #
11
+ # test "has an email" do
12
+ # ...
13
+ # end
14
+ #
15
+ # context "validations" do
16
+ # test "ensure the email can't be blank" do
17
+ # ...
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # Will output, when run with the +:documentation+ report:
23
+ #
24
+ # A user
25
+ # - has a name
26
+ # - has an email
27
+ #
28
+ # A user validations
29
+ # - ensure the email can't be blank
30
+ #
31
+ # This is based on the specdoc runner in rspec[http://rspec.info].
32
+ class Reports::Documentation < Report
33
+ include Utils::Summaries
34
+ include Utils::ColorfulOutput
35
+
36
+ attr_reader :stream #:nodoc:
37
+
38
+ # Set the stream where the report will be written to. STDOUT by default.
39
+ def initialize(stream=STDOUT)
40
+ @stream = stream
41
+ end
42
+
43
+ on :enter do |report, context|
44
+ report.puts context.description unless context.tests.empty?
45
+ end
46
+
47
+ on :pass do |report, passed_test|
48
+ report.puts "- #{passed_test.test_name}", :passed
49
+ end
50
+
51
+ on :failure do |report, failed_test|
52
+ position = report.failures_and_errors.index(failed_test) + 1
53
+ report.puts "- #{failed_test.test_name} (#{position})", :failed
54
+ end
55
+
56
+ on :error do |report, errored_test|
57
+ position = report.failures_and_errors.index(errored_test) + 1
58
+ report.puts "- #{errored_test.test_name} (#{position})", :errored
59
+ end
60
+
61
+ on :pending do |report, pending_test|
62
+ report.puts "- #{pending_test.test_name} (#{pending_test.pending_message})", :pending
63
+ end
64
+
65
+ on :exit do |report, context|
66
+ report.puts unless context.tests.empty?
67
+ end
68
+
69
+ on :end do |report|
70
+ report.summarize_pending_tests
71
+ report.summarize_errors
72
+ report.summarize_test_totals
73
+ end
74
+ end
75
+
76
+ add_report :documentation, Reports::Documentation
77
+ end
@@ -0,0 +1,46 @@
1
+ module Protest
2
+ # The +:progress+ report will output a +.+ for each passed test in the suite,
3
+ # a +P+ for each pending test, an +F+ for each test that failed an assertion,
4
+ # and an +E+ for each test that raised an unrescued exception.
5
+ #
6
+ # At the end of the suite it will output a list of all pending tests, with
7
+ # files and line numbers, and after that a list of all failures and errors,
8
+ # which also contains the first 3 lines of the backtrace for each.
9
+ class Reports::Progress < Report
10
+ include Utils::Summaries
11
+ include Utils::ColorfulOutput
12
+
13
+ attr_reader :stream #:nodoc:
14
+
15
+ # Set the stream where the report will be written to. STDOUT by default.
16
+ def initialize(stream=STDOUT)
17
+ @stream = stream
18
+ end
19
+
20
+ on :end do |report|
21
+ report.puts
22
+ report.puts
23
+ report.summarize_pending_tests
24
+ report.summarize_errors
25
+ report.summarize_test_totals
26
+ end
27
+
28
+ on :pass do |report, pass|
29
+ report.print ".", :passed
30
+ end
31
+
32
+ on :pending do |report, pending|
33
+ report.print "P", :pending
34
+ end
35
+
36
+ on :failure do |report, failure|
37
+ report.print "F", :failed
38
+ end
39
+
40
+ on :error do |report, error|
41
+ report.print "E", :errored
42
+ end
43
+ end
44
+
45
+ add_report :progress, Reports::Progress
46
+ end
@@ -0,0 +1,42 @@
1
+ module Protest
2
+ class Runner
3
+ # Set up the test runner. Takes in a report that will be passed to test
4
+ # cases for reporting.
5
+ def initialize(report)
6
+ @report = report
7
+ end
8
+
9
+ # Run a set of test cases, provided as arguments. This will fire relevant
10
+ # events on the runner's report, at the +start+ and +end+ of the test run,
11
+ # and before and after each test case (+enter+ and +exit+.)
12
+ def run(*test_cases)
13
+ @report.on_start if @report.respond_to?(:on_start)
14
+ test_cases.each do |test_case|
15
+ @report.on_enter(test_case) if @report.respond_to?(:on_enter)
16
+ test_case.run(self)
17
+ @report.on_exit(test_case) if @report.respond_to?(:on_exit)
18
+ end
19
+ @report.on_end if @report.respond_to?(:on_end)
20
+ end
21
+
22
+ # Run a test and report if it passes, fails, or is pending. Takes the name
23
+ # of the test as an argument. You can avoid reporting a passed test by
24
+ # passing +false+ as a second argument.
25
+ def report(test, report_success=true)
26
+ @report.on_test(Test.new(test)) if @report.respond_to?(:on_test)
27
+ yield
28
+ @report.on_pass(PassedTest.new(test)) if report_success
29
+ rescue Pending => e
30
+ @report.on_pending(PendingTest.new(test, e))
31
+ rescue AssertionFailed => e
32
+ @report.on_failure(FailedTest.new(test, e))
33
+ rescue Exception => e
34
+ @report.on_error(ErroredTest.new(test, e))
35
+ end
36
+
37
+ def assert(condition, message) #:nodoc:
38
+ @report.add_assertion
39
+ raise AssertionFailed, message unless condition
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,221 @@
1
+ module Protest
2
+ # Define a top level test context where to define tests. This works exactly
3
+ # the same as subclassing TestCase explicitly.
4
+ #
5
+ # Protest.context "A user" do
6
+ # ...
7
+ # end
8
+ #
9
+ # is just syntax sugar to write:
10
+ #
11
+ # class TestUser < Protest::TestCase
12
+ # self.description = "A user"
13
+ # ...
14
+ # end
15
+ def self.context(description, &block)
16
+ TestCase.context(description, &block)
17
+ end
18
+
19
+ class << self
20
+ alias_method :describe, :context
21
+ alias_method :story, :context
22
+ end
23
+
24
+ # A TestCase defines a suite of related tests. You can further categorize
25
+ # your tests by declaring nested contexts inside the class. See
26
+ # TestCase.context.
27
+ class TestCase
28
+ # Run all tests in this context. Takes a Report instance in order to
29
+ # provide output.
30
+ def self.run(result)
31
+ result.report("#{description} global setup", false) { do_global_setup }
32
+ tests.each {|test| test.run(result) }
33
+ result.report("#{description} global teardown", false) { do_global_teardown }
34
+ end
35
+
36
+ # Tests added to this context.
37
+ def self.tests
38
+ @tests ||= []
39
+ end
40
+
41
+ # Add a test to be run in this context. This method is aliased as +it+ and
42
+ # +should+ for your comfort.
43
+ def self.test(name, &block)
44
+ tests << new(name, &block)
45
+ end
46
+
47
+ # Add a setup block to be run before each test in this context. This method
48
+ # is aliased as +before+ for your comfort.
49
+ def self.setup(&block)
50
+ define_method :setup do
51
+ super
52
+ instance_eval(&block)
53
+ end
54
+ end
55
+
56
+ # Add a +setup+ block that will be run *once* for the entire test case,
57
+ # before the first test is run.
58
+ #
59
+ # Keep in mind that while +setup+ blocks are evaluated on the context of the
60
+ # test, and thus you can share state between them, your tests will not be
61
+ # able to access instance variables set in a +global_setup+ block.
62
+ #
63
+ # This is usually not needed (and generally using it is a code smell, since
64
+ # you could make a test dependent on the state of other tests, which is a
65
+ # huge problem), but it comes in handy when you need to do expensive
66
+ # operations in your test setup/teardown and the tests won't modify the
67
+ # state set on this operations. For example, creating large amount of
68
+ # records in a database or filesystem, when your tests will only read these
69
+ # records.
70
+ def self.global_setup(&block)
71
+ (class << self; self; end).class_eval do
72
+ define_method :do_global_setup do
73
+ super
74
+ instance_eval(&block)
75
+ end
76
+ end
77
+ end
78
+
79
+ # Add a teardown block to be run after each test in this context. This
80
+ # method is aliased as +after+ for your comfort.
81
+ def self.teardown(&block)
82
+ define_method :teardown do
83
+ instance_eval(&block)
84
+ super
85
+ end
86
+ end
87
+
88
+ # Add a +teardown+ block that will be run *once* for the entire test case,
89
+ # after the last test is run.
90
+ #
91
+ # Keep in mind that while +teardown+ blocks are evaluated on the context of
92
+ # the test, and thus you can share state between the tests and the
93
+ # teardown blocks, you will not be able to access instance variables set in
94
+ # a test from your +global_teardown+ block.
95
+ #
96
+ # See TestCase.global_setup for a discussion on why these methods are best
97
+ # avoided unless you really need them and use them carefully.
98
+ def self.global_teardown(&block)
99
+ (class << self; self; end).class_eval do
100
+ define_method :do_global_teardown do
101
+ instance_eval(&block)
102
+ super
103
+ end
104
+ end
105
+ end
106
+
107
+
108
+ # Define a new test context nested under the current one. All +setup+ and
109
+ # +teardown+ blocks defined on the current context will be inherited by the
110
+ # new context. This method is aliased as +describe+ for your comfort.
111
+ def self.context(description, &block)
112
+ subclass = Class.new(self)
113
+ subclass.class_eval(&block) if block
114
+ subclass.description = description
115
+ const_set(sanitize_description(description), subclass)
116
+ end
117
+
118
+ class << self
119
+ # Fancy name for your test case, reports can use this to give nice,
120
+ # descriptive output when running your tests.
121
+ attr_accessor :description
122
+
123
+ alias_method :describe, :context
124
+ alias_method :story, :context
125
+
126
+ alias_method :before, :setup
127
+ alias_method :after, :teardown
128
+
129
+ alias_method :before_all, :global_setup
130
+ alias_method :after_all, :global_setup
131
+
132
+ alias_method :it, :test
133
+ alias_method :should, :test
134
+ alias_method :scenario, :test
135
+ end
136
+
137
+ # Initialize a new instance of a single test. This test can be run in
138
+ # isolation by calling TestCase#run.
139
+ def initialize(name, &block)
140
+ @test = block
141
+ @name = name
142
+ end
143
+
144
+ # Run a test in isolation. Any +setup+ and +teardown+ blocks defined for
145
+ # this test case will be run as expected.
146
+ #
147
+ # You need to provide a Runner instance to handle errors/pending tests/etc.
148
+ #
149
+ # If the test's block is nil, then the test will be marked as pending and
150
+ # nothing will be run.
151
+ def run(runner)
152
+ @runner = runner
153
+
154
+ runner.report(self) do
155
+ pending if test.nil?
156
+
157
+ setup
158
+ instance_eval(&test)
159
+ teardown
160
+ end
161
+ end
162
+
163
+ # Ensure a condition is met. This will raise AssertionFailed if the
164
+ # condition isn't met. You can override the default failure message
165
+ # by passing it as an argument.
166
+ def assert(condition, message="Expected condition to be satisfied")
167
+ @runner.assert(condition, message)
168
+ end
169
+
170
+ # Provided for Test::Unit compatibility, this lets you include
171
+ # Test::Unit::Assertions and everything works seamlessly.
172
+ def assert_block(message="Expected condition to be satisified") #:nodoc:
173
+ assert(yield, message)
174
+ end
175
+
176
+ # Make the test be ignored as pending. You can override the default message
177
+ # that will be sent to the report by passing it as an argument.
178
+ def pending(message="Not Yet Implemented")
179
+ raise Pending, message
180
+ end
181
+
182
+ # Name of the test
183
+ def name
184
+ @name
185
+ end
186
+
187
+ private
188
+
189
+ def setup #:nodoc:
190
+ end
191
+
192
+ def teardown #:nodoc:
193
+ end
194
+
195
+ def test
196
+ @test
197
+ end
198
+
199
+ def self.sanitize_description(description)
200
+ "Test#{description.gsub(/\W+/, ' ').strip.gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
201
+ end
202
+ private_class_method :sanitize_description
203
+
204
+ def self.do_global_setup
205
+ end
206
+ private_class_method :do_global_setup
207
+
208
+ def self.do_global_teardown
209
+ end
210
+ private_class_method :do_global_teardown
211
+
212
+ def self.description #:nodoc:
213
+ parent = ancestors[1..-1].detect {|a| a < Protest::TestCase }
214
+ "#{parent.description rescue nil} #{@description}".strip
215
+ end
216
+
217
+ def self.inherited(child)
218
+ Protest.add_test_case(child)
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,90 @@
1
+ module Protest
2
+ # Encapsulates the relevant information about a test. Useful for certain
3
+ # reports.
4
+ class Test
5
+ # Instance of the test case that was run.
6
+ attr_reader :test
7
+
8
+ # Name of the test that passed. Useful for certain reports.
9
+ attr_reader :test_name
10
+
11
+ def initialize(test) #:nodoc:
12
+ @test = test
13
+ @test_name = test.name
14
+ end
15
+ end
16
+
17
+ # Mixin for tests that had an error (this could be either a failed assertion,
18
+ # unrescued exceptions, or just a pending tests.)
19
+ module TestWithErrors
20
+ # The triggered exception (AssertionFailed, Pending, or any
21
+ # subclass of Exception in the case of an ErroredTest.)
22
+ attr_reader :error
23
+
24
+ # Message with which it failed the assertion.
25
+ def error_message
26
+ error.message
27
+ end
28
+
29
+ # Line of the file where the assertion failed.
30
+ def line
31
+ backtrace.first.split(":")[1]
32
+ end
33
+
34
+ # File where the assertion failed.
35
+ def file
36
+ backtrace.first.split(":")[0]
37
+ end
38
+
39
+ # Filtered backtrace of the assertion. See Protest::Utils::BacktraceFilter
40
+ # for details on the filtering.
41
+ def backtrace
42
+ @backtrace ||= Utils::BacktraceFilter.filter(raw_backtrace)
43
+ end
44
+
45
+ # Raw backtrace, as provided by the error.
46
+ def raw_backtrace
47
+ error.backtrace
48
+ end
49
+ end
50
+
51
+ # Encapsulate the relevant information for a test that passed.
52
+ class PassedTest < Test
53
+ end
54
+
55
+ # Encapsulates the relevant information for a test which failed an
56
+ # assertion.
57
+ class FailedTest < Test
58
+ include TestWithErrors
59
+
60
+ def initialize(test, error) #:nodoc:
61
+ super(test)
62
+ @error = error
63
+ end
64
+ end
65
+
66
+ # Encapsulates the relevant information for a test which raised an
67
+ # unrescued exception.
68
+ class ErroredTest < Test
69
+ include TestWithErrors
70
+
71
+ def initialize(test, error) #:nodoc:
72
+ super(test)
73
+ @error = error
74
+ end
75
+ end
76
+
77
+ # Encapsulates the relevant information for a test that the user marked as
78
+ # pending.
79
+ class PendingTest < Test
80
+ include TestWithErrors
81
+
82
+ # Message passed to TestCase#pending, if any.
83
+ alias_method :pending_message, :error_message
84
+
85
+ def initialize(test, error) #:nodoc:
86
+ super(test)
87
+ @error = error
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,6 @@
1
+ module Protest
2
+ # Utility mixins used in the framework, or available to Report authors to
3
+ # provide common functionality to the reports.
4
+ module Utils
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ module Protest
2
+ module Utils
3
+ # Small utility object to filter an error's backtrace and remove any mention
4
+ # of Testicle's own files.
5
+ module BacktraceFilter
6
+ # Path to the library's 'lib' dir.
7
+ BASE_PATH = /^#{Regexp.escape(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__)))))}/
8
+
9
+ # Filter the backtrace, removing any reference to files located in
10
+ # BASE_PATH.
11
+ def self.filter(backtrace)
12
+ backtrace.reject do |line|
13
+ file = line.split(":").first
14
+ File.expand_path(file) =~ BASE_PATH
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ module Protest
2
+ module Utils
3
+ # Mixin that provides colorful output to your console based reports. This uses
4
+ # bash's escape sequences, so it won't work on windows.
5
+ #
6
+ # TODO: Make this work on windows with ansicolor or whatever the gem is named
7
+ module ColorfulOutput
8
+ # Returns a hash with the color values for different states. Override this
9
+ # method safely to change the output colors. The defaults are:
10
+ #
11
+ # :passed:: Light green
12
+ # :pending:: Light yellow
13
+ # :errored:: Light purple
14
+ # :failed:: Light red
15
+ #
16
+ # See http://www.hypexr.org/bash_tutorial.php#colors for a description of
17
+ # Bash color codes.
18
+ def self.colors
19
+ { :passed => "1;32",
20
+ :pending => "1;33",
21
+ :errored => "1;35",
22
+ :failed => "1;31" }
23
+ end
24
+
25
+ class << self
26
+ # Whether to use colors in the output or not. The default is +true+.
27
+ attr_accessor :colorize
28
+ end
29
+
30
+ self.colorize = true
31
+
32
+ # Print the string followed by a newline to whatever IO stream is defined in
33
+ # the method #stream using the correct color depending on the state passed.
34
+ def puts(string=nil, state=:normal)
35
+ if string.nil? # calling IO#puts with nil is not the same as with no args
36
+ stream.puts
37
+ else
38
+ stream.puts colorize(string, state)
39
+ end
40
+ end
41
+
42
+ # Print the string to whatever IO stream is defined in the method #stream
43
+ # using the correct color depending on the state passed.
44
+ def print(string=nil, state=:normal)
45
+ if string.nil? # calling IO#puts with nil is not the same as with no args
46
+ stream.print
47
+ else
48
+ stream.print colorize(string, state)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def colorize(string, state)
55
+ if state == :normal || !ColorfulOutput.colorize
56
+ string
57
+ else
58
+ "\033[#{color_for_state(state)}m#{string}\033[0m"
59
+ end
60
+ end
61
+
62
+ def color_for_state(state)
63
+ ColorfulOutput.colors.fetch(state)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,116 @@
1
+ module Protest
2
+ module Utils
3
+ # Mixin that provides summaries for your text based test runs.
4
+ module Summaries
5
+ # Call on +:end+ to output the amount of tests (passed, pending, failed
6
+ # and errored), the amount of assertions, and the time elapsed.
7
+ #
8
+ # For example:
9
+ #
10
+ # on :end do |report|
11
+ # report.puts
12
+ # report.summarize_test_totals
13
+ # end
14
+ #
15
+ # This relies on the public Report API, and on the presence of a #puts
16
+ # method to write to whatever source you are writing your report.
17
+ def summarize_test_totals
18
+ puts test_totals
19
+ puts running_time
20
+ end
21
+
22
+ # Call on +:end+ to print a list of pending tests, including file and line
23
+ # number where the call to TestCase#pending+ was made.
24
+ #
25
+ # It will not output anything if there weren't any pending tests.
26
+ #
27
+ # For example:
28
+ #
29
+ # on :end do |report|
30
+ # report.puts
31
+ # report.summarize_pending_tests
32
+ # end
33
+ #
34
+ # This relies on the public Report API, and on the presence of a #puts
35
+ # method to write to whatever source you are writing your report.
36
+ def summarize_pending_tests
37
+ return if pendings.empty?
38
+
39
+ puts "Pending tests:"
40
+ puts
41
+
42
+ pad_indexes = pendings.size.to_s.size
43
+ pendings.each_with_index do |pending, index|
44
+ puts " #{pad(index+1, pad_indexes)}) #{pending.test_name} (#{pending.pending_message})", :pending
45
+ puts indent("On line #{pending.line} of `#{pending.file}'", 6 + pad_indexes), :pending
46
+ puts
47
+ end
48
+ end
49
+
50
+ # Call on +:end+ to print a list of failures (failed assertions) and errors
51
+ # (unrescued exceptions), including file and line number where the test
52
+ # failed, and a short backtrace.
53
+ #
54
+ # It will not output anything if there weren't any pending tests.
55
+ #
56
+ # For example:
57
+ #
58
+ # on :end do |report|
59
+ # report.puts
60
+ # report.summarize_pending_tests
61
+ # end
62
+ #
63
+ # This relies on the public Report API, and on the presence of a #puts
64
+ # method to write to whatever source you are writing your report.
65
+ def summarize_errors
66
+ return if failures_and_errors.empty?
67
+
68
+ puts "Failures:"
69
+ puts
70
+
71
+ pad_indexes = failures_and_errors.size.to_s.size
72
+ failures_and_errors.each_with_index do |error, index|
73
+ colorize_as = ErroredTest === error ? :errored : :failed
74
+ puts " #{pad(index+1, pad_indexes)}) #{test_type(error)}: `#{error.test_name}' (on line #{error.line} of `#{error.file}')", colorize_as
75
+ puts indent("With `#{error.error_message}'", 6 + pad_indexes), colorize_as
76
+ indent(error.backtrace[0..2], 6 + pad_indexes).each {|backtrace| puts backtrace, colorize_as }
77
+ puts
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def running_time
84
+ "Ran in #{time_elapsed} seconds"
85
+ end
86
+
87
+ def test_totals
88
+ "%d test%s, %d assertion%s (%d passed, %d pending, %d failed, %d errored)" % [total_tests,
89
+ total_tests == 1 ? "" : "s",
90
+ assertions,
91
+ assertions == 1 ? "" : "s",
92
+ passes.size,
93
+ pendings.size,
94
+ failures.size,
95
+ errors.size]
96
+ end
97
+
98
+ def indent(strings, size=2, indent_with=" ")
99
+ Array(strings).map do |str|
100
+ str.to_s.split("\n").map {|s| indent_with * size + s }.join("\n")
101
+ end
102
+ end
103
+
104
+ def pad(str, amount)
105
+ " " * (amount - str.to_s.size) + str.to_s
106
+ end
107
+
108
+ def test_type(test)
109
+ case test # order is important since ErroredTest < FailedTest
110
+ when ErroredTest; "Error"
111
+ when FailedTest; "Failure"
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,38 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "protest"
3
+ s.version = "0.2"
4
+ s.date = "2009-09-11"
5
+
6
+ s.description = "Protest is a tiny, simple, and easy-to-extend test framework"
7
+ s.summary = s.description
8
+ s.homepage = "http://github.com/foca/protest"
9
+
10
+ s.authors = ["Nicolás Sanguinetti"]
11
+ s.email = "contacto@nicolassanguinetti.info"
12
+
13
+ s.require_paths = ["lib"]
14
+ s.rubyforge_project = "protest"
15
+ s.has_rdoc = true
16
+ s.rubygems_version = "1.3.1"
17
+
18
+ s.files = %w[
19
+ .gitignore
20
+ LICENSE
21
+ README.rdoc
22
+ Rakefile
23
+ protest.gemspec
24
+ lib/protest.rb
25
+ lib/protest/utils.rb
26
+ lib/protest/utils/backtrace_filter.rb
27
+ lib/protest/utils/summaries.rb
28
+ lib/protest/utils/colorful_output.rb
29
+ lib/protest/test_case.rb
30
+ lib/protest/tests.rb
31
+ lib/protest/runner.rb
32
+ lib/protest/report.rb
33
+ lib/protest/reports.rb
34
+ lib/protest/reports/progress.rb
35
+ lib/protest/reports/documentation.rb
36
+ lib/protest/rails.rb
37
+ ]
38
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protest
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.2"
5
+ platform: ruby
6
+ authors:
7
+ - "Nicol\xC3\xA1s Sanguinetti"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-11 00:00:00 -03:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Protest is a tiny, simple, and easy-to-extend test framework
17
+ email: contacto@nicolassanguinetti.info
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - .gitignore
26
+ - LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - protest.gemspec
30
+ - lib/protest.rb
31
+ - lib/protest/utils.rb
32
+ - lib/protest/utils/backtrace_filter.rb
33
+ - lib/protest/utils/summaries.rb
34
+ - lib/protest/utils/colorful_output.rb
35
+ - lib/protest/test_case.rb
36
+ - lib/protest/tests.rb
37
+ - lib/protest/runner.rb
38
+ - lib/protest/report.rb
39
+ - lib/protest/reports.rb
40
+ - lib/protest/reports/progress.rb
41
+ - lib/protest/reports/documentation.rb
42
+ - lib/protest/rails.rb
43
+ has_rdoc: true
44
+ homepage: http://github.com/foca/protest
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project: protest
67
+ rubygems_version: 1.3.2
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Protest is a tiny, simple, and easy-to-extend test framework
71
+ test_files: []
72
+