protest 0.2

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.
@@ -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
+