sunspot_matchers_testunit 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :gemcutter
2
+
3
+ # Specify your gem's dependencies in sunspot_matchers_testunit.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sunspot_matchers_testunit (1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ builder (3.0.0)
10
+ escape (0.0.4)
11
+ pr_geohash (1.0.0)
12
+ rake (0.8.7)
13
+ rsolr (0.12.1)
14
+ builder (>= 2.1.2)
15
+ sunspot (1.2.1)
16
+ escape (= 0.0.4)
17
+ pr_geohash (~> 1.0)
18
+ rsolr (= 0.12.1)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ bundler (>= 1.0.0)
25
+ rake
26
+ sunspot (~> 1.2.1)
27
+ sunspot_matchers_testunit!
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Joseph Palermo, Pivotal Labs, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE
data/README.markdown ADDED
@@ -0,0 +1,238 @@
1
+ # Sunspot Matchers
2
+
3
+ [Sunspot](http://outoftime.github.com/sunspot/) is a great Ruby library for constructing searches against Solr. However,
4
+ because of the way the Sunspot DSL is constructed, it can be difficult to do simple assertions about your searches
5
+ without doing full integration tests.
6
+
7
+ The goal of these matchers are to make it easier to unit test search logic without having to construct the individual
8
+ fixture scenarios inside of Solr and then actually perform a search against Solr.
9
+
10
+ This is a direct port of the excellent [Sunspot Matchers](http://github.com/pivotal/sunspot_matchers) library by Joseph
11
+ Palermo, rewritten for use with Test::Unit.
12
+
13
+ # Installation
14
+
15
+ You will need to replace the Sunspot Session object with the spy provided. You can do this globally by putting the
16
+ following in a setup block or your test_helper.
17
+
18
+ def setup
19
+ Sunspot.session = SunspotMatchers::SunspotSessionSpy.new(Sunspot.session)
20
+ end
21
+
22
+ Keep in mind, this will prevent any test from actually hitting Solr, so if you have integration tests, you'll either
23
+ need to be more careful which tests you replace the session for, or you'll need to restore the original session before
24
+ those tests
25
+
26
+ Sunspot.session = Sunspot.session.original_session
27
+
28
+ You will also need to include the matchers in your tests. Again, this can be done globally in your test_helper.
29
+
30
+ require 'sunspot_matchers_testunit'
31
+ include SunspotMatchersTestunit
32
+
33
+ Alternately, you could include them into individual tests if needed.
34
+
35
+ # Matchers
36
+
37
+ ## assert_is_search_for
38
+
39
+ If you perform a search against your Post model, you could write this assertion:
40
+
41
+ `assert_is_search_for Sunspot.session, Post`
42
+
43
+ Individual searches are stored in an array, so if you perform multiple, you'll have to match against them manually. Without
44
+ an explicit search specified, it will use the last one.
45
+
46
+ `assert_is_search_for Sunspot.session.searches.first, Post`
47
+
48
+ ## assert_has_search_params
49
+
50
+ This is where the bulk of the functionality lies. There are seven types of search matches you can perform: `keywords`,
51
+ `with`, `without`, `paginate`, `order_by`, `facet`, and `boost`.
52
+
53
+ In all of the examples below, the arguments fully match the search terms. This is not expected or required. You can
54
+ have a dozen `with` restrictions on a search and still write an expectation on a single one of them.
55
+
56
+ Negative expectations also work correctly. `assert_has_no_search_params` will fail if the search actually includes the
57
+ provided arguments.
58
+
59
+ With all matchers, you can specify a `Proc` as the second argument, and perform multi statement expectations inside the
60
+ Proc. Keep in mind, that only the search type specified in the first argument will actually be checked. So if you specify
61
+ `keywords` and `with` restrictions in the same Proc, but you said `assert_has_search_params Sunspot.session, :keywords, ...`
62
+ the `with` restrictions are simply ignored.
63
+
64
+ ### wildcard matching
65
+
66
+ keywords, with, without, and order_by support wildcard expectations using the `any_param` parameter:
67
+
68
+ Sunspot.search(Post) do
69
+ with :blog_id, 4
70
+ order_by :blog_id, :desc
71
+ end
72
+
73
+ assert_has_search_params Sunspot.session, [ :with, :blog_id, any_param ]
74
+ assert_has_search_params Sunspot.session, [ :order_by, :blog_id, any_param ]
75
+ assert_has_search_params Sunspot.session, [ :order_by, any_param ]
76
+ assert_has_no_search_params Sunspot.session, [ :order_by, :category_ids, any_param ]
77
+
78
+ ### :keywords
79
+
80
+ You can match against a keyword search:
81
+
82
+ Sunspot.search(Post) do
83
+ keywords 'great pizza'
84
+ end
85
+
86
+ assert_has_search_params Sunspot.session, [ :keywords, 'great pizza' ]
87
+
88
+ ### :with
89
+
90
+ You can match against a with restriction:
91
+
92
+ Sunspot.search(Post) do
93
+ with :author_name, 'Mark Twain'
94
+ end
95
+
96
+ assert_has_search_params Sunspot.session, [ :with, :author_name, 'Mark Twain' ]
97
+
98
+ Complex conditions can be matched by using a Proc instead of a value. Be aware that order does matter, not for
99
+ the actual results that would come out of Solr, but the matcher will fail of the order of `with` restrictions is
100
+ different.
101
+
102
+ Sunspot.search(Post) do
103
+ any_of do
104
+ with :category_ids, 1
105
+ with :category_ids, 2
106
+ end
107
+ end
108
+
109
+ assert_has_search_params Sunspot.session, [ :with, Proc.new {
110
+ any_of do
111
+ with :category_ids, 1
112
+ with :category_ids, 2
113
+ end
114
+ } ]
115
+
116
+ ### :without
117
+
118
+ Without is nearly identical to with:
119
+
120
+ Sunspot.search(Post) do
121
+ without :author_name, 'Mark Twain'
122
+ end
123
+
124
+ assert_has_search_params Sunspot.session, [ :without, :author_name, 'Mark Twain' ]
125
+
126
+ ### :paginate
127
+
128
+ You can also specify only page or per_page, both are not required.
129
+
130
+ Sunspot.search(Post) do
131
+ paginate :page => 3, :per_page => 15
132
+ end
133
+
134
+ assert_has_search_params Sunspot.session, [ :paginate, :page => 3, :per_page => 15 ]
135
+
136
+ ### :order_by
137
+
138
+ Expectations on multiple orderings are supported using using the Proc format mentioned above.
139
+
140
+ Sunspot.search(Post) do
141
+ order_by :published_at, :desc
142
+ end
143
+
144
+ assert_has_search_params Sunspot.session, [ :order_by, :published_at, :desc ]
145
+
146
+ ### :facet
147
+
148
+ Standard faceting expectation:
149
+
150
+ Sunspot.search(Post) do
151
+ facet :category_ids
152
+ end
153
+
154
+ assert_has_search_params Sunspot.session, [ :facet, :category_ids ]
155
+
156
+ Faceting where a query is excluded:
157
+
158
+ Sunspot.search(Post) do
159
+ category_filter = with(:category_ids, 2)
160
+ facet(:category_ids, :exclude => category_filter)
161
+ end
162
+
163
+ assert_has_search_params Sunspot.session, [ :facet, Proc.new {
164
+ category_filter = with(:category_ids, 2)
165
+ facet(:category_ids, :exclude => category_filter)
166
+ } ]
167
+
168
+ Query faceting:
169
+
170
+ Sunspot.search(Post) do
171
+ facet(:average_rating) do
172
+ row(1.0..2.0) do
173
+ with(:average_rating, 1.0..2.0)
174
+ end
175
+ row(2.0..3.0) do
176
+ with(:average_rating, 2.0..3.0)
177
+ end
178
+ end
179
+ end
180
+
181
+ assert_has_search_params Sunspot.session, [ :facet, Proc.new {
182
+ facet(:average_rating) do
183
+ row(1.0..2.0) do
184
+ with(:average_rating, 1.0..2.0)
185
+ end
186
+ row(2.0..3.0) do
187
+ with(:average_rating, 2.0..3.0)
188
+ end
189
+ end
190
+ } ]
191
+
192
+ ### :boost
193
+
194
+ Field boost matching:
195
+
196
+ Sunspot.search(Post) do
197
+ keywords 'great pizza' do
198
+ boost_fields :body => 2.0
199
+ end
200
+ end
201
+
202
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
203
+ keywords 'great pizza' do
204
+ boost_fields :body => 2.0
205
+ end
206
+ } ]
207
+
208
+ Boost query matching:
209
+
210
+ Sunspot.search(Post) do
211
+ keywords 'great pizza' do
212
+ boost(2.0) do
213
+ with :blog_id, 4
214
+ end
215
+ end
216
+ end
217
+
218
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
219
+ keywords 'great pizza' do
220
+ boost(2.0) do
221
+ with :blog_id, 4
222
+ end
223
+ end
224
+ } ]
225
+
226
+ Boost function matching:
227
+
228
+ Sunspot.search(Post) do
229
+ keywords 'great pizza' do
230
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
231
+ end
232
+ end
233
+
234
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
235
+ keywords 'great pizza' do
236
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
237
+ end
238
+ } ]
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,2 @@
1
+ require 'sunspot_matchers_testunit/matchers'
2
+ require 'sunspot_matchers_testunit/sunspot_session_spy'
@@ -0,0 +1,317 @@
1
+ require 'test/unit/assertions'
2
+
3
+ module SunspotMatchersTestunit
4
+ class BaseMatcher
5
+ attr_accessor :args
6
+
7
+ def initialize(session, args)
8
+ @session = session
9
+ @args = args
10
+ build_comparison_search
11
+ end
12
+
13
+ def build_comparison_search
14
+ @comparison_search = if(@args.last.is_a?(Proc))
15
+ SunspotMatchersTestunit::SunspotSessionSpy.new(@session).build_search(search_types, &args.last)
16
+ else
17
+ SunspotMatchersTestunit::SunspotSessionSpy.new(@session).build_search(search_types) do
18
+ send(search_method, *args)
19
+ end
20
+ end
21
+ end
22
+
23
+ def search_tuple
24
+ search_tuple = @session.is_a?(Array) ? @session : @session.searches.last
25
+ raise 'no search found' unless search_tuple
26
+ search_tuple
27
+ end
28
+
29
+ def actual_search
30
+ search_tuple.last
31
+ end
32
+
33
+ def search_types
34
+ search_tuple.first
35
+ end
36
+
37
+ def wildcard?
38
+ @args && @args.last == any_param
39
+ end
40
+
41
+ def field
42
+ @args && @args.first
43
+ end
44
+
45
+ def query_params_for_search(search)
46
+ search.instance_variable_get(:@query).to_params
47
+ end
48
+
49
+ def actual_params
50
+ @actual_params ||= query_params_for_search(actual_search)
51
+ end
52
+
53
+ def comparison_params
54
+ @comparison_params ||= query_params_for_search(@comparison_search)
55
+ end
56
+
57
+ def match?
58
+ differences.empty?
59
+ end
60
+
61
+ def missing_param_error_message
62
+ missing_params = differences
63
+ actual_values = missing_params.keys.collect {|key| "#{key} => #{actual_params[key]}"}
64
+ missing_values = missing_params.collect{ |key, value| "#{key} => #{value}"}
65
+ "expected search params: #{actual_values.join(' and ')} to match expected: #{missing_values.join(' and ')}"
66
+ end
67
+
68
+ def unexpected_match_error_message
69
+ actual_values = keys_to_compare.collect {|key| "#{key} => #{actual_params[key]}"}
70
+ comparison_values = keys_to_compare.collect {|key| "#{key} => #{comparison_params[key]}"}
71
+ "expected search params: #{actual_values.join(' and ')} NOT to match expected: #{comparison_values.join(' and ')}"
72
+ end
73
+
74
+ def differences
75
+ keys_to_compare.inject({}) do |hsh, key|
76
+ result = compare_key(key)
77
+ hsh[key] = result unless result.empty?
78
+ hsh
79
+ end
80
+ end
81
+
82
+ def compare_key(key)
83
+ if(actual_params[key].is_a?(Array) || comparison_params[key].is_a?(Array))
84
+ compare_multi_value(actual_params[key], comparison_params[key])
85
+ else
86
+ compare_single_value(actual_params[key], comparison_matcher_for_key(key))
87
+ end
88
+ end
89
+
90
+ def comparison_matcher_for_key(key)
91
+ if wildcard? && wildcard_matcher_for_keys.has_key?(key)
92
+ wildcard_matcher_for_keys[key]
93
+ else
94
+ comparison_params[key]
95
+ end
96
+ end
97
+
98
+ def compare_single_value(actual, comparison)
99
+ if comparison.is_a?(Regexp)
100
+ return [] if comparison =~ actual
101
+ return [comparison.source]
102
+ end
103
+ return [comparison] unless actual == comparison
104
+ []
105
+ end
106
+
107
+ def compare_multi_value(actual, comparison)
108
+ filter_values(comparison).reject do |value|
109
+ next false unless actual
110
+ value_matcher = Regexp.new(Regexp.escape(value))
111
+ actual.any?{ |actual_value| actual_value =~ value_matcher }
112
+ end
113
+ end
114
+
115
+ def filter_values(values)
116
+ return values unless wildcard?
117
+ field_matcher = Regexp.new(field.to_s)
118
+ values.select{ |value| field_matcher =~ value }.collect{|value| value.gsub(/:.*/, '')}
119
+ end
120
+
121
+ def wildcard_matcher_for_keys
122
+ {}
123
+ end
124
+ end
125
+
126
+ class HaveSearchParams
127
+ include Test::Unit::Assertions
128
+
129
+ def initialize(session, method, *args)
130
+ @session = session
131
+ @method = method
132
+ @args = args
133
+ end
134
+
135
+ def get_matcher
136
+ matcher_class = case @method
137
+ when :with
138
+ WithMatcher
139
+ when :without
140
+ WithoutMatcher
141
+ when :keywords
142
+ KeywordsMatcher
143
+ when :boost
144
+ BoostMatcher
145
+ when :facet
146
+ FacetMatcher
147
+ when :order_by
148
+ OrderByMatcher
149
+ when :paginate
150
+ PaginationMatcher
151
+ end
152
+ matcher_class.new(@session, @args)
153
+ end
154
+ end
155
+
156
+ def assert_has_search_params(session, method_and_args)
157
+ method, *args = method_and_args
158
+ matcher = HaveSearchParams.new(session, method, *args).get_matcher
159
+ assert matcher.match?, matcher.missing_param_error_message
160
+ end
161
+
162
+ def assert_has_no_search_params(session, method_and_args)
163
+ method, *args = method_and_args
164
+ matcher = HaveSearchParams.new(session, method, *args).get_matcher
165
+ assert !matcher.match?, matcher.unexpected_match_error_message
166
+ end
167
+
168
+ class WithMatcher < BaseMatcher
169
+ def search_method
170
+ :with
171
+ end
172
+
173
+ def keys_to_compare
174
+ [:fq]
175
+ end
176
+ end
177
+
178
+ class WithoutMatcher < BaseMatcher
179
+ def search_method
180
+ :without
181
+ end
182
+
183
+ def keys_to_compare
184
+ [:fq]
185
+ end
186
+ end
187
+
188
+ class KeywordsMatcher < BaseMatcher
189
+ def search_method
190
+ :keywords
191
+ end
192
+
193
+ def keys_to_compare
194
+ [:q, :qf]
195
+ end
196
+
197
+ def wildcard_matcher_for_keys
198
+ {:q => /./, :qf => /./}
199
+ end
200
+ end
201
+
202
+ class BoostMatcher < BaseMatcher
203
+ def search_method
204
+ :boost
205
+ end
206
+
207
+
208
+ def keys_to_compare
209
+ [:qf, :bq, :bf]
210
+ end
211
+ end
212
+
213
+ class FacetMatcher < BaseMatcher
214
+ def search_method
215
+ :facet
216
+ end
217
+
218
+ def keys_to_compare
219
+ comparison_params.keys.select {|key| /facet/ =~ key.to_s}
220
+ end
221
+ end
222
+
223
+ class OrderByMatcher < BaseMatcher
224
+ def search_method
225
+ :order_by
226
+ end
227
+
228
+ def keys_to_compare
229
+ [:sort]
230
+ end
231
+
232
+ def wildcard_matcher_for_keys
233
+ return {:sort => /./} if field_wildcard?
234
+ param = comparison_params[:sort]
235
+ regex = Regexp.new(param.gsub(any_param, '.*'))
236
+ {:sort => regex}
237
+ end
238
+
239
+ def field_wildcard?
240
+ @args.first == any_param
241
+ end
242
+
243
+ def direction_wildcard?
244
+ @args.length == 2 && @args.last == any_param
245
+ end
246
+
247
+ def args
248
+ return @args unless direction_wildcard?
249
+ @args[0...-1] + [:asc]
250
+ end
251
+
252
+ def build_comparison_search
253
+ if field_wildcard?
254
+ @comparison_params = {:sort => any_param}
255
+ elsif direction_wildcard?
256
+ super
257
+ @comparison_params = comparison_params
258
+ @comparison_params[:sort].gsub!("asc", any_param)
259
+ else
260
+ super
261
+ end
262
+ end
263
+ end
264
+
265
+ class PaginationMatcher < BaseMatcher
266
+ def search_method
267
+ :paginate
268
+ end
269
+
270
+ def keys_to_compare
271
+ [:rows, :start]
272
+ end
273
+ end
274
+
275
+ class BeASearchFor
276
+ def initialize(session, expected_class)
277
+ @session = session
278
+ @expected_class = expected_class
279
+ end
280
+
281
+ def match?
282
+ search_types.include?(@expected_class)
283
+ end
284
+
285
+ def search_tuple
286
+ search_tuple = @session.is_a?(Array) ? @session : @session.searches.last
287
+ raise 'no search found' unless search_tuple
288
+ search_tuple
289
+ end
290
+
291
+ def search_types
292
+ search_tuple.first
293
+ end
294
+
295
+ def failure_message_for_should
296
+ "expected search class: #{search_types.join(' and ')} to match expected class: #{@expected_class}"
297
+ end
298
+
299
+ def failure_message_for_should_not
300
+ "expected search class: #{search_types.join(' and ')} NOT to match expected class: #{@expected_class}"
301
+ end
302
+ end
303
+
304
+ def assert_is_search_for(session, expected_class)
305
+ matcher = BeASearchFor.new(session, expected_class)
306
+ assert matcher.match?, matcher.failure_message_for_should
307
+ end
308
+
309
+ def assert_is_not_search_for(session, expected_class)
310
+ matcher = BeASearchFor.new(session, expected_class)
311
+ assert !matcher.match?, matcher.failure_message_for_should_not
312
+ end
313
+ end
314
+
315
+ def any_param
316
+ "ANY_PARAM"
317
+ end