aspec 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ aspec
2
+ =====
3
+
4
+ External api surface testing library
5
+
6
+ # Installing
7
+
8
+ gem install aspec
9
+
10
+ # Running
11
+
12
+ aspec spec/thing_aspec.rb
13
+
14
+ # Tests
15
+
16
+ rspec spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path("../../lib", __FILE__))
4
+ require 'aspec'
5
+
6
+ Aspec::CLI.new(Dir.pwd, ARGV).run
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+
3
+ require 'rack/test'
4
+ require 'rspec/mocks/standalone'
5
+ require 'term/ansicolor'
6
+ require 'json'
7
+
8
+ require 'aspec/cli'
9
+ require 'aspec/formatters/terminal'
10
+ require 'aspec/formatters/junit'
11
+ require 'aspec/runner'
12
+ require 'aspec/parser'
13
+ require 'aspec/test'
14
+
15
+ module Aspec
16
+ def self.configuration
17
+ @configuration ||= Configure.new
18
+ end
19
+
20
+ def self.configure
21
+ yield configuration
22
+ end
23
+
24
+ class Configure
25
+ attr_accessor :verbose, :slow, :formatter
26
+
27
+ def verbose?; verbose; end
28
+ def slow?; slow; end
29
+
30
+ def app_under_test(&block)
31
+ @app_under_test = block
32
+ end
33
+
34
+ def before(&block)
35
+ @before = block
36
+ end
37
+
38
+ def get_app_under_test
39
+ @app_under_test
40
+ end
41
+
42
+ def get_before
43
+ @before
44
+ end
45
+
46
+ def after_suite(&block)
47
+ @after_suite = block
48
+ end
49
+
50
+ def get_after_suite
51
+ @after_suite
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,45 @@
1
+
2
+ module Aspec
3
+ class CLI
4
+ attr_reader :args, :working_dir
5
+
6
+ def initialize(working_dir, args)
7
+ @working_dir = working_dir
8
+ @args = args
9
+ end
10
+
11
+ def aspec_files
12
+ files = []
13
+ @args.each do |arg|
14
+ arg = File.expand_path(arg, working_dir)
15
+ if File.exist?(arg)
16
+ if File.directory?(arg)
17
+ files += Dir[arg + "**/*.aspec"]
18
+ elsif arg =~ /.*\.aspec/
19
+ files << arg
20
+ end
21
+ end
22
+ end
23
+ files
24
+ end
25
+
26
+ def aspec_helper_path
27
+ File.expand_path("aspec/aspec_helper.rb")
28
+ end
29
+
30
+ def run
31
+ bits = args[0].split(":")
32
+
33
+ load aspec_helper_path if File.exist?(aspec_helper_path)
34
+
35
+ @lines = bits[1..-1].map(&:to_i)
36
+ is_verbose = args.include?("-v")
37
+ Aspec.configure do |c|
38
+ c.verbose = is_verbose
39
+ c.slow = args.include?("--slow")
40
+ c.formatter = args.include?("--junit") ? Formatter::JUnit.new(@file) : Formatter::Terminal.new(is_verbose)
41
+ end
42
+ TestRunner.new(Aspec.configuration, aspec_files).run(@lines)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,92 @@
1
+
2
+ module Aspec
3
+ module Formatter
4
+ class JUnit
5
+ def initialize(test_file_name, verbose = false, out_file_name = '.junit_aspecs')
6
+ @test_results = { :failures => [], :successes => [] }
7
+ @out = File.open(out_file_name, 'w')
8
+ @test_file_name = test_file_name
9
+ @exceptions = []
10
+ at_exit do
11
+ unless @out.closed?
12
+ @out.flush
13
+ @out.close
14
+ puts "Output junit results to .junit_aspecs"
15
+ end
16
+ end
17
+ end
18
+
19
+ def clear
20
+ end
21
+
22
+ def comment(comment_string)
23
+ end
24
+
25
+ def step_error(step)
26
+ @test_results[:failures] << [step_line(step), @exceptions]
27
+ @exceptions = []
28
+ end
29
+
30
+ def exception(error_string)
31
+ @exceptions << error_string
32
+ end
33
+
34
+ def step_error_title(step)
35
+ end
36
+
37
+ def step_pass(step)
38
+ @test_results[:successes] << step_line(step)
39
+ end
40
+
41
+ def debug
42
+ end
43
+
44
+ def dump_summary(summary)
45
+ @out.puts("<?xml version=\"1.0\" encoding=\"utf-8\" ?>")
46
+ @out.puts("<testsuite errors=\"0\" failures=\"#{failure_count}\" tests=\"#{example_count}\" time=\"#{duration=0}\" timestamp=\"#{Time.now.iso8601}\">")
47
+ @out.puts(" <properties />")
48
+
49
+ @test_results[:successes].each do |success_string|
50
+ #TODO: Add timings
51
+ runtime = 0
52
+ @out.puts(" <testcase classname=\"#{@test_file_name}\" name=\"#{test_name(success_string)}\" time=\"#{runtime}\" />")
53
+ end
54
+ @test_results[:failures].each do |(failure_string, exceptions)|
55
+ runtime = 0
56
+ @out.puts(" <testcase classname=\"#{@test_file_name}\" name=\"#{failure_string}\" time=\"#{runtime}\">")
57
+
58
+ @out.puts(" <failure message=\"failure\" type=\"failure\">")
59
+ @out.puts("<![CDATA[ #{exceptions} ]]>")
60
+ @out.puts(" </failure>")
61
+ @out.puts(" </testcase>")
62
+ end
63
+ @out.puts("</testsuite>")
64
+ end
65
+
66
+ private
67
+ def step_line(step)
68
+ "#{step[:method]} #{step[:url]} line: #{step[:line_num]}"
69
+ end
70
+
71
+ def test_name(string)
72
+ xml_encode(string.split("\n")[0].strip)
73
+ end
74
+
75
+ def example_count
76
+ @test_results[:successes].count + failure_count
77
+ end
78
+
79
+ def failure_count
80
+ @test_results[:failures].count
81
+ end
82
+
83
+ def xml_encode(string)
84
+ #TODO: Use builder to do this
85
+ string.gsub!('&', '&amp;')
86
+ string.gsub!('>', '')
87
+ string.gsub!('"','&quot;')
88
+ string
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,80 @@
1
+
2
+ module Aspec
3
+ module Formatter
4
+ class Terminal
5
+ include Term::ANSIColor
6
+
7
+ def initialize(verbose, out = STDOUT)
8
+ @out = out
9
+ @verbose = verbose
10
+ @line_buffer ||= []
11
+ end
12
+
13
+ def clear
14
+ @line_buffer.clear
15
+ end
16
+
17
+ def comment(comment_string)
18
+ line(comment_string)
19
+ end
20
+
21
+ def exception(error_string)
22
+ line(error_string)
23
+ end
24
+
25
+ def step_error_title(step)
26
+ step_line(step)
27
+ end
28
+
29
+ def step_error(step)
30
+ line(red + step_line(step) + reset)
31
+ print_error unless @verbose
32
+ end
33
+
34
+ def step_pass(step)
35
+ line(green + step_line(step) + reset)
36
+ end
37
+
38
+ def debug(step)
39
+ @out.puts(step.inspect)
40
+ end
41
+
42
+ def dump_summary(summary)
43
+ @out.puts summary
44
+ end
45
+
46
+ private
47
+
48
+ def step_line(step)
49
+ bits = [step[:method].rjust(7, " "), step[:url].ljust(50, " "), step[:exp_status], (step[:exp_content_type]||"")]
50
+ if step[:exp_content_type] == "application/json"
51
+ begin
52
+ json_string = JSON.parse(step[:exp_response]).to_json
53
+ if json_string.length > 20
54
+ json_string = "\n" + JSON.pretty_generate(JSON.parse(step[:exp_response])).split("\n").map {|l| " \\ #{l}"}.join("\n")
55
+ end
56
+ bits << json_string
57
+ rescue JSON::ParserError
58
+ bits << step[:exp_response]
59
+ end
60
+ else
61
+ bits << step[:exp_response]
62
+ end
63
+ bits.join("\t")
64
+ end
65
+
66
+ def line(line_string)
67
+ @out.puts(line_string) if @verbose
68
+ @line_buffer << line_string
69
+ end
70
+
71
+ def print_error
72
+ @line_buffer.each do |line|
73
+ @out.puts line
74
+ end
75
+ @line_buffer = []
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ module Aspec
2
+ class Parser
3
+ def initialize(source)
4
+ @lines = source.split("\n").map {|l| l.strip}
5
+ end
6
+
7
+ def tests
8
+ @tests ||= begin
9
+ tests = [[]]
10
+ @lines.each_with_index do |line, line_num|
11
+ if line =~ /^\s*$/
12
+ if tests.last.length > 0
13
+ tests << []
14
+ end
15
+ elsif line =~ /^\s*\\(.*)$/
16
+ tests.last.last[:exp_response] = (tests.last.last[:exp_response] + $1)
17
+ else
18
+ tests.last << parse_line(line, line_num)
19
+ end
20
+ end
21
+ tests.select {|tests| tests.any?}.map {|steps| Test.new(steps) }
22
+ end
23
+ end
24
+
25
+ def parse_line(line, line_num)
26
+ if line =~ /^\s*(#.*)$/
27
+ {:comment => $1, :line_num => line_num}
28
+ else
29
+ bits = line.split(" ")
30
+ method = bits[0]
31
+ url = bits[1]
32
+ url = URI.encode(url)
33
+
34
+ exp_status = bits[2]
35
+ exp_status = exp_status.strip if exp_status
36
+ exp_content_type = bits[3]
37
+ exp_content_type = exp_content_type.strip if exp_content_type
38
+ exp_response = (bits[4..-1]||[]).join(" ")
39
+ is_regex = exp_response[0] == '/' and exp_response[-1] == '/' and exp_response.size > 2
40
+ exp_response = exp_response[1 .. -2] if is_regex
41
+
42
+ {:method => method, :url => url,
43
+ :exp_status => exp_status, :exp_content_type => exp_content_type, :exp_response => exp_response,
44
+ :resp_is_regex => is_regex, :line_num => line_num
45
+ }
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,76 @@
1
+
2
+ module Aspec
3
+ class TestRunner
4
+ include Term::ANSIColor
5
+
6
+ attr_reader :config, :source
7
+
8
+ def initialize(config, paths)
9
+ @paths = paths
10
+ @config = config
11
+ end
12
+
13
+ def verbose?
14
+ config.verbose?
15
+ end
16
+
17
+ def slow?
18
+ config.slow?
19
+ end
20
+
21
+ def formatter
22
+ config.formatter
23
+ end
24
+
25
+ def tests
26
+ @tests ||= begin
27
+ result = []
28
+ @paths.each do |path|
29
+ parser = Parser.new(File.read(path))
30
+ result += parser.tests
31
+ end
32
+ result.flatten
33
+ end
34
+ end
35
+
36
+ def before_each
37
+ if before_block = config.get_before
38
+ before_block.call
39
+ end
40
+ end
41
+
42
+ def run(lines)
43
+ successes = 0
44
+ failures = 0
45
+ if lines.any?
46
+ run_tests = tests.select {|test| lines.any? {|line_num| test.contains_line?(line_num)}}
47
+ else
48
+ run_tests = tests
49
+ end
50
+
51
+ run_tests.each do |test|
52
+ before_each
53
+ if test.run(config)
54
+ successes += 1 unless test.comment_only?
55
+ puts if verbose?
56
+ else
57
+ failures += 1
58
+ puts
59
+ end
60
+ formatter.clear
61
+ end
62
+ color = send(failures > 0 ? :red : :green)
63
+ formatter.dump_summary color + "#{successes} passed, #{failures} failed." + reset
64
+
65
+ if after_suite_block = config.get_after_suite
66
+ after_suite_block.call
67
+ end
68
+ if failures > 0
69
+ exit(1)
70
+ else
71
+ exit(0)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,140 @@
1
+
2
+ module Aspec
3
+ class Test
4
+ include Rack::Test::Methods
5
+
6
+ def initialize(steps)
7
+ @steps = steps
8
+ end
9
+
10
+ def validate_method(method)
11
+ raise "unknown method #{method}" unless %W(GET POST DELETE PUT).include?(method)
12
+ end
13
+
14
+ def comment_only?
15
+ @steps.all? {|s| s[:comment]}
16
+ end
17
+
18
+ def contains_line?(line_num)
19
+ @steps.first[:line_num] <= line_num and @steps.last[:line_num] >= line_num
20
+ end
21
+
22
+ def app
23
+ @app
24
+ end
25
+
26
+ def run(config)
27
+ formatter = config.formatter
28
+ is_slow = config.slow?
29
+ @app = config.get_app_under_test.call
30
+ start_time = Time.at(0)
31
+ failed = false
32
+ @steps.each_with_index do |step, time_delta|
33
+ if is_slow
34
+ sleep 0.5
35
+ end
36
+
37
+ if ARGV.include?("--debug")
38
+ formatter.debug(step)
39
+ end
40
+
41
+ if failed
42
+ formatter.step_error_title(step)
43
+ next
44
+ end
45
+
46
+ if step[:comment]
47
+ formatter.comment(step[:comment])
48
+ else
49
+ Time.stub!(:now).and_return(start_time + 2*time_delta)
50
+
51
+ begin
52
+ if step[:method][0] == ">"
53
+ method = step[:method][1..-1]
54
+ validate_method(method)
55
+ url = "http://" + step[:url]
56
+ FakeWeb.register_uri(method.downcase.to_sym, url,
57
+ :body => step[:exp_response],
58
+ :content_type => step[:exp_content_type]
59
+ )
60
+ else
61
+ validate_method(step[:method])
62
+ send(step[:method].downcase, step[:url])
63
+ end
64
+ rescue Object => e
65
+ formatter.exception(" " + e.class.to_s + ": " + e.message)
66
+ e.backtrace.each do |backtrace_line|
67
+ formatter.exception(" " + backtrace_line) unless backtrace_line =~ /vendor\/bundle/ or backtrace_line =~ /test.rb/
68
+ end
69
+ failed = true
70
+ end
71
+
72
+ unless failed or step[:method][0] == ">"
73
+ if last_response.status.to_s != step[:exp_status]
74
+ formatter.exception(" * Expected status #{step[:exp_status]} got #{last_response.status}")
75
+ failed = true
76
+ end
77
+
78
+ if step[:exp_content_type] == "application/json" && !step[:resp_is_regex]
79
+ begin
80
+ expected_object = JSON.parse(step[:exp_response])
81
+ begin
82
+ response_object = JSON.parse(last_response.body)
83
+ if expected_object != response_object
84
+ formatter.exception(" * Expected response #{JSON.pretty_generate(expected_object)} got #{JSON.pretty_generate(response_object)}")
85
+ failed = true
86
+ end
87
+ rescue JSON::ParserError
88
+ formatter.exception(" * Response did not parse correctly as JSON: #{last_response.body.inspect}")
89
+ failed = true
90
+ end
91
+ rescue JSON::ParserError
92
+ formatter.exception(" * Expectation did not parse correctly as JSON: #{step[:exp_response].inspect}")
93
+ failed = true
94
+ end
95
+
96
+ else
97
+
98
+ if step[:resp_is_regex]
99
+ pattern = nil, body = nil
100
+ if !(step[:exp_content_type].start_with? 'text/')
101
+ pattern = Regexp.new(step[:exp_response].force_encoding("ASCII-8BIT"), Regexp::FIXEDENCODING)
102
+ body = last_response.body.to_s.force_encoding("ASCII-8BIT")
103
+ else
104
+ pattern = Regexp.new(step[:exp_response])
105
+ body = last_response.body.to_s
106
+ end
107
+ if !(body =~ pattern)
108
+ formatter.exception(" * Expected response pattern #{step[:exp_response].inspect} didn't match #{last_response.body.inspect}")
109
+ failed = true
110
+ end
111
+ elsif !step[:resp_is_regex] & (last_response.body.to_s != step[:exp_response])
112
+ formatter.exception(" * Expected response #{step[:exp_response].inspect} got #{last_response.body.inspect[0..50] + "..."}")
113
+ failed = true
114
+ end
115
+ end
116
+
117
+ if step[:exp_content_type]
118
+ exp_content_type_header = "#{step[:exp_content_type]}"
119
+ exp_content_type_header << ";charset=utf-8" unless exp_content_type_header.start_with? "image/"
120
+ if last_response.headers["Content-Type"] != exp_content_type_header
121
+ formatter.exception(" * Expected content type #{exp_content_type_header} got #{last_response.headers["Content-Type"]}")
122
+ failed = true
123
+ end
124
+ end
125
+ end
126
+
127
+ if failed
128
+ formatter.step_error(step)
129
+ else
130
+ formatter.step_pass(step)
131
+ end
132
+ end
133
+ end
134
+ !failed
135
+ end
136
+ end
137
+ end
138
+
139
+
140
+
@@ -0,0 +1,9 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe Aspec do
5
+ it "Should exist" do
6
+ Aspec
7
+ end
8
+
9
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aspec::CLI do
4
+ def test_app_dir
5
+ File.expand_path("../test_app", __FILE__)
6
+ end
7
+
8
+ it "should list all files in a directory" do
9
+ cli = Aspec::CLI.new(test_app_dir, ["aspec/"])
10
+ cli.aspec_files.sort.should == [
11
+ File.expand_path("aspec/failing.aspec", test_app_dir),
12
+ File.expand_path("aspec/passing.aspec", test_app_dir)].sort
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ $:.push(File.expand_path("../../lib", __FILE__))
2
+ require 'aspec'
@@ -0,0 +1,8 @@
1
+ $:.push(File.expand_path("../../", __FILE__))
2
+ require 'test_app'
3
+
4
+ Aspec.configure do |c|
5
+ c.app_under_test do
6
+ TestApp.new
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ class TestApp < Sinatra::Application
5
+ get "/" do
6
+ end
7
+
8
+ get "/artists" do
9
+ status 200
10
+ content_type :json
11
+ @@artists.to_json
12
+ end
13
+
14
+ post "/artists" do
15
+ @@artists ||= []
16
+ @@artists << params[:name]
17
+ status 204
18
+ nil
19
+ end
20
+
21
+ delete "/artists/:name" do
22
+ @@artists = @@artists.reject {|a| a == params[:name] }
23
+ status 204
24
+ nil
25
+ end
26
+ end
27
+
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aspec
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: "0.1"
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Lucraft
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-12-06 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: term-ansicolor
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: rack-test
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id003
48
+ description:
49
+ email: dan@songkick.com
50
+ executables:
51
+ - aspec
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - README.md
56
+ files:
57
+ - README.md
58
+ - lib/aspec.rb
59
+ - lib/aspec/parser.rb
60
+ - lib/aspec/runner.rb
61
+ - lib/aspec/cli.rb
62
+ - lib/aspec/test.rb
63
+ - lib/aspec/formatters/terminal.rb
64
+ - lib/aspec/formatters/junit.rb
65
+ - spec/spec_helper.rb
66
+ - spec/cli_spec.rb
67
+ - spec/test_app/aspec/aspec_helper.rb
68
+ - spec/test_app/test_app.rb
69
+ - spec/aspec_spec.rb
70
+ - bin/aspec
71
+ homepage: http://github.com/songkick/aspec
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options:
76
+ - --main
77
+ - README.md
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.12
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Testing for API external surfaces
99
+ test_files: []
100
+