Browse files

Initial commit.

  • Loading branch information...
0 parents commit 5438ed2af1279fbc00aad027d4aa56c77077d2f7 @eric1234 committed Jun 26, 2010
113 README
@@ -0,0 +1,113 @@
+= An Experiment
+
+First let me say up front that this library is an experiment. I don't
+even know if it is a good idea yet but if you are feeling adventurous
+I encourage you to take the red pill and see how deep this rabbit hole
+goes.
+
+= Putting Tests In-line
+
+So the basic goal of this library is to allow you put your test right
+next to the code being tested. For example:
+
+ def multiply(a, b)
+ a * b
+ end
+
+ Test do
+ assert_equal 6, multiply(2, 3)
+ assert_equal 10, multiply(5, 10)
+ assert_not_equal 42, multiply(6, 9)
+ end
+
+Lets go over a few of the key features of in-line testing.
+
+== Optional Naming
+
+The name of a test is often used just to know what code it is testing.
+For example under traditional Test::Unit I might call the test
+"test_multiply". But this name is entirely redundant when you start
+placing your tests next to the code being tested. It is obvious what is
+being testing. When a tests fails we are often just looking at the line
+number so naming the test doesn't really help there.
+
+This doesn't mean I am against naming test. For example you may have
+multiple test blocks that are testing a single method. You want them
+in multiple blocks because each has a specific purpose. So you may
+want to give them a name to let others know of that purpose. For
+example we might write the above as:
+
+ def multiply(a, b)
+ a * b
+ end
+
+ Test 'success' do
+ assert_equal 6, multiply(2, 3)
+ assert_equal 10, multiply(5, 10)
+ end
+
+ Test 'failure' do
+ assert_not_equal 42, multiply(6, 9)
+ end
+
+Here we have separated our success testing and our failure testing into
+separate test blocks. But even here the name is just a label. In fact
+we may later have another set of tests that have a "success" and
+"failure" block. This is allowed (multiple test blocks can have the
+same label).
+
+== Test By Running
+
+Another advantage to having tests and code together is that you can
+test a file just by running it (assuming the code is written to be
+fairly independent). No need to have fancy rake tasks or test suites.
+Just execute the file you are currently coding and the results are
+returned to you.
+
+Of course you can still run a test suite of your entire project if you
+want but sometimes it is nice to be able to pass around a file that
+contains the documentation, the code and the test all together and you
+can just run it to verify it is working.
+
+== Rails integration
+
+We also provide Rails integration. Simply include this gem in your Rails
+project and add the following lines to your Rakefile:
+
+ require 'test_inline'
+ require 'test_inline/rails_tasks'
+
+Now you will be able to run your unit tests with a simple:
+
+ rake test:inline:units
+
+You will be able to run your functional test with:
+
+ rake test:inline:functionals
+
+Or you can run both with just:
+
+ rake test:inline
+
+Finally note that our Rails integration will make the TestCase inherit
+from the right classes in the Rails framework. So for example
+you can just call the normal "get, post, etc" methods in your controller
+tests, you can access the Rails specific assertions (such as
+assert_redirected_to). You can just place your tests right in with the
+controllers, helpers and models and it will do the right thing.
+
+== Wrapper for Test::Unit
+
+We are currently just wrapping Test::Unit. This has the advantage of
+building on something well used and standard in the Ruby world. It means
+that any traditional Test::Unit can be converted over to test_inline
+mostly by just copying and pasting. This does cause problems in the fact
+that Test::Unit has certain concepts that we don't use (like named test
+cases and named test methods). For test cases we just generate anonymous
+classes. For test methods we generate unique name (possibly based on the
+optional label).
+
+This of course opens up the question of would it be a good idea to allow
+wrapping of other test environments like RSpec, Cucumber, etc. Those
+questions are left to others as Test::Unit is good enough for most
+Ruby projects.
18 Rakefile
@@ -0,0 +1,18 @@
+require 'rake/testtask'
+require 'rake/gempackagetask'
+
+Rake::TestTask.new do |t|
+ t.libs << "lib"
+ t.test_files = FileList['test/*_test.rb']
+end
+task :default => :test
+
+spec = eval File.read('test_inline.gemspec')
+Rake::GemPackageTask.new spec do |pkg|
+ pkg.need_tar = false
+end
+
+desc "Publish gem to rubygems.org"
+task :publish => :package do
+ `gem push pkg/#{spec.name}-#{spec.version}.gem`
+end
68 example.rb
@@ -0,0 +1,68 @@
+# An example of a simple class that uses inline testing
+
+$LOAD_PATH << 'lib'
+require 'test_inline'
+
+class Version
+ attr_reader :major, :minor, :patch_level
+
+ # New Version object with given major, minor and patch_level versions.
+ def initialize(major, minor, patch_level)
+ @major, @minor, @patch_level =
+ major.to_i, minor.to_i, patch_level.to_i
+ end
+ Test do
+ v = Version.new 0, 1, 2
+ assert_equal 0, v.major
+ assert_equal 1, v.minor
+ assert_equal 2, v.patch_level
+ end
+
+ # Will allow you to treat a Version as a String
+ def to_str
+ [major, minor, patch_level] * '.'
+ end
+ alias_method :to_s, :to_str
+ Test do
+ assert_equal '0.1.2', Version.new(0, 1, 2).to_str
+ assert_equal '0.1.2', Version.new(0, 1, 2).to_s
+ end
+
+ # Compare two version numbers.
+ # Mixes in Comparable to make all operators work
+ def <=>(other)
+ raise ArgumentError, 'Must be a Version object' unless other.is_a? Version
+ to_str <=> other.to_str
+ end
+ include Comparable
+ Test do
+ v1 = Version.new 1, 2, 3
+ v2 = Version.new 3, 1, 4
+
+ assert_equal -1, (v1 <=> v2)
+ assert_equal 1, (v2 <=> v1)
+ assert_equal 0, (v1 <=> v1)
+
+ assert (v1 < v2)
+ assert (v2 > v1)
+ assert (v1 == v1)
+ assert v2.between?(v1, Version.new(9,4,3))
+
+ assert_raises(ArgumentError) {v1 <=> '1.2.3'}
+ end
+
+ # Will take a version string, parse it and return a Version object
+ def self.parse(str)
+ raise ArgumentError, 'Invalid version format' if str !~ /^\d+\.\d+\.\d+$/
+ new *str.split('.')
+ end
+ Test do
+ v = Version.parse '0.1.2'
+ assert_equal 0, v.major
+ assert_equal 1, v.minor
+ assert_equal 2, v.patch_level
+
+ assert_raises(ArgumentError) {Version.parse '0.1.2a'}
+ end
+
+end
36 lib/core_ext.rb
@@ -0,0 +1,36 @@
+# This file contains logic that enhances core Ruby functionality but
+# not anything that is specifically related to test_inline. Just stuff
+# to make Ruby more expressive.
+
+# ActiveSupport includes a number of goodies we use. Most people have
+# it due to the proliferation of Rails so rather than duplicating the
+# functionality we will just add the dependency.
+require 'rubygems'
+require 'active_support'
+
+module Kernel
+ # A bit like __FILE__ but returns the file of the code that called
+ # the current method instead of the file of the current location.
+ def calling_file
+ begin
+ raise StandardError
+ rescue
+ $!.backtrace[2].split(':', 2).first
+ end
+ end
+end
+
+class String
+ class << self
+ # Returns a random string of length characters using the given
+ # characterset (default to alphanumeric)
+ def rand length, characters=String.alpha_numeric_charset
+ (1..length).inject('') {|m, i| m + characters.to_a.random_element.to_s}
+ end
+
+ # Returns an array of all alphanumeric characters
+ def alpha_numeric_charset
+ ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a
+ end
+ end
+end
84 lib/test_inline.rb
@@ -0,0 +1,84 @@
+require 'core_ext'
+
+module Test
+ module Inline
+
+ # Will registar parent classes for the TestCase subclasses that
+ # will be created. This is useful to provide default behavior of
+ # a test case such as setup, teardown or private methods across
+ # a general class of files. This is used heavily for the Rails
+ # integration.
+ def self.register_abstract_test_case(path_regexp, klass, &callback)
+ @abstract_test_cases ||= []
+ @abstract_test_cases.unshift({
+ :path_regexp => path_regexp,
+ :klass => klass,
+ :callback => callback || proc {|x, y|}
+ })
+ end
+
+ # Will initialize inline tests located on files in the given paths
+ # and run them in the Test::Unit environment.
+ def self.setup *paths
+ @paths ||= []
+ @paths += paths
+
+ # First setup should init Test::Unit and default parent class
+ if @abstract_test_cases.nil?
+ require 'test/unit'
+ register_abstract_test_case /./, Test::Unit::TestCase
+ end
+ end
+
+ # Will get the TestCase for the given path
+ def self.test_case_for(path)
+ @test_cases ||= {}
+ path = File.expand_path path
+ return @test_cases[path] if @test_cases.has_key? path
+ config = @abstract_test_cases.find {|c| path =~ c[:path_regexp]}
+ klass = Class.new config[:klass]
+ config[:callback].call klass, path
+ @test_cases[path] = klass
+ end
+
+ # Will return the paths that are setup for testing
+ def self.paths; @paths end
+
+ # Will reset remove all tests already setup.
+ def self.reset # :nodoc:
+ @paths = nil
+ @abstract_test_cases = nil
+ @test_cases = nil
+ end
+ end
+end
+
+module Kernel
+
+ # Will add the test to the list of tests run run. Will run the tests
+ # automatically if file with inline tests is simply run.
+ def Test name=String.rand(5), &blk
+ modify_inline_test_case calling_file do |tc|
+ name = "test_#{name.gsub /\W+/, '_'}00000" unless name =~ /\d{5}$/
+ name = name.succ while tc.instance_methods.include? name.to_sym
+ tc.send :define_method, name, &blk
+ end
+ end
+
+ def ForTest &blk
+ modify_inline_test_case(calling_file) {|tc| tc.class_eval &blk}
+ end
+
+ private
+
+ def modify_inline_test_case path, &blk
+ path = File.expand_path path
+ Test::Inline.setup path if
+ Test::Inline.paths.nil? && (File.expand_path($0) == path)
+ if Test::Inline.paths &&
+ Test::Inline.paths.any? {|p| path.starts_with? File.expand_path(p)}
+ tc = Test::Inline.test_case_for path
+ blk.call tc
+ end
+ end
+end
8 lib/test_inline/rails_tasks.rb
@@ -0,0 +1,8 @@
+# Some glue to make it easy to include Rails-specific rake tasks in
+# your Rails application. Simply put the following at the bottom of
+# your Rakefile:
+#
+# require 'test_inline'
+# require 'test_inline/rails_tasks'
+glob = "#{Gem.searcher.find('test_inline').full_gem_path}/rails/tasks/*.rake"
+Dir[glob].each {|ext| load ext}
BIN pkg/test_inline-0.0.1.gem
Binary file not shown.
23 rails/init.rb
@@ -0,0 +1,23 @@
+Test::Inline.register_abstract_test_case \
+ Regexp.new(Regexp.escape(Rails.root.join('app/models'))),
+ ActiveRecord::TestCase
+Test::Inline.register_abstract_test_case \
+ Regexp.new(Regexp.escape(Rails.root.join('app/controllers'))),
+ ActionController::TestCase do |klass, path|
+ zap = Regexp.escape Rails.root.join('app/controllers')
+ path = path.sub Regexp.new('^' + zap), ''
+ path = path.sub(/\.rb$/, '')
+ klass.controller_class = path.camelize.constantize
+end
+require 'action_view/test_case'
+Test::Inline.register_abstract_test_case \
+ Regexp.new(Regexp.escape(Rails.root.join('app/helpers'))),
+ ActionView::TestCase do |klass, path|
+ zap = Regexp.escape Rails.root.join('app/helpers')
+ path = path.sub Regexp.new('^' + zap), ''
+ path = path.sub(/\.rb/, '')
+ klass.helper_class = path.camelize.constantize
+end
+Test::Inline.register_abstract_test_case \
+ Regexp.new(Regexp.escape(Rails.root.join('lib'))),
+ ActiveSupport::TestCase
30 rails/tasks/test_inline.rake
@@ -0,0 +1,30 @@
+namespace :test do
+ namespace :inline do
+
+ desc "Run inline tests in app/models, app/helpers and lib"
+ task :units => 'db:test:prepare' do
+ fork do
+ require Rails.root.join('test/test_helper')
+ paths = %w(app/models app/helpers lib).collect {|p| Rails.root.join p}
+ Test::Inline.setup *paths
+ paths.each {|p| Dir["#{p}/**/*.rb"].each {|f| require f}}
+ Test::Unit::AutoRunner.run
+ end
+ Process.wait
+ end
+
+ desc "Run inline tests in app/controllers"
+ task :functionals => 'db:test:prepare' do
+ fork do
+ require Rails.root.join('test/test_helper')
+ path = Rails.root.join('app/controllers')
+ Test::Inline.setup path
+ Dir["#{path}/**/*.rb"].each {|f| require f}
+ Test::Unit::AutoRunner.run
+ end
+ Process.wait
+ end
+
+ end
+ task :inline => ['test:inline:units', 'test:inline:functionals']
+end
27 test/core_ext_test.rb
@@ -0,0 +1,27 @@
+require 'test/unit'
+require 'core_ext'
+
+class KernerlTest < Test::Unit::TestCase
+ def test_calling_file
+ assert_match /core_ext_test\.rb$/, foo
+ end
+
+ private
+ def foo; calling_file end
+end
+
+class StringTest < Test::Unit::TestCase
+ def test_alpha_numeric_charset
+ explicit = %w(
+ a b c d e f g h i j k l m n o p q r s t u v w x y z
+ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+ ) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ assert_equal explicit, String.alpha_numeric_charset
+ end
+
+ def test_rand
+ assert_equal 5, String.rand(5).length
+ assert_match /^\w+$/, String.rand(20)
+ assert_match /^\d+$/, String.rand(20, 0..9)
+ end
+end
70 test/test_inline_test.rb
@@ -0,0 +1,70 @@
+require 'test/unit'
+require 'test_inline'
+
+class TestInlineTest < Test::Unit::TestCase
+
+ def setup
+ Test::Inline.reset
+ end
+
+ # Verify we can call Test and it is a noop if we are not running tests
+ def test_registration_noop
+ Test {}
+ assert_nil Test::Inline.paths
+ end
+
+ # Register a test with a custom parent and injected functionality
+ def test_registration_with_parent
+ Test::Inline.setup __FILE__
+ parent = Class.new Test::Unit::TestCase
+ parent.send(:define_method, :priv) {'baz'}
+ Test::Inline.register_abstract_test_case /_test\.rb$/, parent do |klass, path|
+ klass.send(:define_method, :another) {path}
+ end
+ Test('xyz') {priv}
+ Test('abc') {cat}
+ ForTest {def cat; 'dog' end}
+ assert Test::Inline.test_case_for(__FILE__).instance_methods.include?("test_xyz00000")
+ assert_equal 'baz', Test::Inline.test_case_for(__FILE__).new(:test_xyz00000).test_xyz00000
+ assert Test::Inline.test_case_for(__FILE__).instance_methods.include?("test_abc00000")
+ assert_equal 'dog', Test::Inline.test_case_for(__FILE__).new(:test_abc00000).test_abc00000
+ assert_equal File.expand_path(__FILE__),
+ Test::Inline.test_case_for(__FILE__).new(:another).another
+ end
+
+ # Make sure we can call setup multiple times and the paths add
+ def test_setup
+ Test::Inline.setup '/tmp', '/usr'
+ assert_equal ['/tmp', '/usr'], Test::Inline.paths
+ Test::Inline.setup '/etc'
+ assert_equal ['/tmp', '/usr', '/etc'], Test::Inline.paths
+ end
+
+ # If we run the current file without doing setup make sure tests run
+ def test_auto_build
+ old_script = $0
+ $0 = __FILE__
+ Test('def') {'bar'}
+ $0 = old_script
+ assert Test::Inline.test_case_for(__FILE__).instance_methods.include?("test_def00000")
+ assert_equal 'bar', Test::Inline.test_case_for(__FILE__).new(:test_def00000).test_def00000
+ end
+
+ # If we don't provide a name make sure it generates one
+ def test_auto_name
+ Test::Inline.setup __FILE__
+ Test {'no name'}
+ assert !Test::Inline.test_case_for(__FILE__).instance_methods.grep(/^test_/).empty?
+ end
+
+ def test_test_case_for
+ Test::Inline.setup __FILE__
+ a = Test::Inline.test_case_for '/a'
+ b = Test::Inline.test_case_for '/b'
+ c = Test::Inline.test_case_for '/a'
+ assert_equal a.object_id, c.object_id
+ assert_not_equal a.object_id, b.object_id
+ assert_not_equal c.object_id, b.object_id
+ end
+
+end
19 test_inline.gemspec
@@ -0,0 +1,19 @@
+Gem::Specification.new do |s|
+ s.name = 'test_inline'
+ s.version = '0.0.1'
+ s.homepage = 'http://wiki.github.com/eric1234/test_inline/'
+ s.author = 'Eric Anderson'
+ s.email = 'eric@pixelwareinc.com'
+ s.add_dependency 'activesupport'
+ s.files = Dir['lib/**/*.rb'] + Dir['rails/**/*.rb'] + Dir['rails/**/*.rake']
+ s.has_rdoc = true
+ s.extra_rdoc_files << 'README'
+ s.rdoc_options << '--main' << 'README'
+ s.summary = 'Place your automated testing right next to the code being tested'
+ s.description = <<-DESCRIPTION
+ test_inline allows you to place your automated testing right next
+ to the code being tested much like RDoc allows you to put your
+ documentation right next to the code being documented. See the
+ README for the rational for why you would want to do this.
+ DESCRIPTION
+end

0 comments on commit 5438ed2

Please sign in to comment.