better_seo 0.13.0 → 1.0.0.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +121 -3
- data/README.md +299 -181
- data/docs/00_OVERVIEW.md +472 -0
- data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
- data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
- data/docs/03_STRUCTURED_DATA.md +140 -0
- data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
- data/docs/05_RAILS_INTEGRATION.md +175 -0
- data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
- data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
- data/docs/DEPENDENCIES.md +383 -0
- data/docs/README.md +180 -0
- data/docs/TESTING_STRATEGY.md +663 -0
- data/lib/better_seo/analytics/google_analytics.rb +83 -0
- data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
- data/lib/better_seo/configuration.rb +316 -0
- data/lib/better_seo/dsl/base.rb +86 -0
- data/lib/better_seo/dsl/meta_tags.rb +55 -0
- data/lib/better_seo/dsl/open_graph.rb +109 -0
- data/lib/better_seo/dsl/twitter_cards.rb +131 -0
- data/lib/better_seo/errors.rb +31 -0
- data/lib/better_seo/generators/amp_generator.rb +83 -0
- data/lib/better_seo/generators/breadcrumbs_generator.rb +126 -0
- data/lib/better_seo/generators/canonical_url_manager.rb +106 -0
- data/lib/better_seo/generators/meta_tags_generator.rb +100 -0
- data/lib/better_seo/generators/open_graph_generator.rb +110 -0
- data/lib/better_seo/generators/robots_txt_generator.rb +102 -0
- data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
- data/lib/better_seo/image/optimizer.rb +143 -0
- data/lib/better_seo/rails/helpers/controller_helpers.rb +118 -0
- data/lib/better_seo/rails/helpers/seo_helper.rb +176 -0
- data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
- data/lib/better_seo/rails/model_helpers.rb +62 -0
- data/lib/better_seo/rails/railtie.rb +22 -0
- data/lib/better_seo/sitemap/builder.rb +65 -0
- data/lib/better_seo/sitemap/generator.rb +57 -0
- data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
- data/lib/better_seo/sitemap/url_entry.rb +157 -0
- data/lib/better_seo/structured_data/article.rb +55 -0
- data/lib/better_seo/structured_data/base.rb +73 -0
- data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
- data/lib/better_seo/structured_data/event.rb +207 -0
- data/lib/better_seo/structured_data/faq_page.rb +55 -0
- data/lib/better_seo/structured_data/generator.rb +75 -0
- data/lib/better_seo/structured_data/how_to.rb +96 -0
- data/lib/better_seo/structured_data/local_business.rb +94 -0
- data/lib/better_seo/structured_data/organization.rb +67 -0
- data/lib/better_seo/structured_data/person.rb +51 -0
- data/lib/better_seo/structured_data/product.rb +123 -0
- data/lib/better_seo/structured_data/recipe.rb +135 -0
- data/lib/better_seo/validators/seo_recommendations.rb +165 -0
- data/lib/better_seo/validators/seo_validator.rb +195 -0
- data/lib/better_seo/version.rb +1 -1
- data/lib/better_seo.rb +5 -0
- data/lib/generators/better_seo/install_generator.rb +21 -0
- data/lib/generators/better_seo/templates/README +29 -0
- data/lib/generators/better_seo/templates/better_seo.rb +40 -0
- metadata +69 -2
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# BetterSeo - Strategia di Testing
|
|
2
|
+
|
|
3
|
+
Approccio completo per testing di BetterSeo con RSpec.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Obiettivi Testing
|
|
8
|
+
|
|
9
|
+
### Coverage Goals
|
|
10
|
+
|
|
11
|
+
- **Target**: > 90% code coverage
|
|
12
|
+
- **Minimum**: 85% code coverage per release
|
|
13
|
+
- **Critical paths**: 100% coverage (configuration, generators, validators)
|
|
14
|
+
|
|
15
|
+
### Quality Goals
|
|
16
|
+
|
|
17
|
+
- ✅ Tutti i test passano (green) prima di merge
|
|
18
|
+
- ✅ Zero flaky tests
|
|
19
|
+
- ✅ Test performance < 30 secondi suite completa
|
|
20
|
+
- ✅ Isolamento completo tra test (no shared state)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Test Pyramid
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/\
|
|
28
|
+
/ \
|
|
29
|
+
/ E2E\ (5%)
|
|
30
|
+
/______\
|
|
31
|
+
/ \
|
|
32
|
+
/Integration\ (25%)
|
|
33
|
+
/____________\
|
|
34
|
+
/ \
|
|
35
|
+
/ Unit Tests \ (70%)
|
|
36
|
+
/_________________\
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Unit Tests (70%)
|
|
40
|
+
- DSL classes
|
|
41
|
+
- Generators
|
|
42
|
+
- Validators
|
|
43
|
+
- Configuration
|
|
44
|
+
- Helpers individuali
|
|
45
|
+
|
|
46
|
+
### Integration Tests (25%)
|
|
47
|
+
- Rails integration
|
|
48
|
+
- Generator + Validator flow
|
|
49
|
+
- Controller + Helper interaction
|
|
50
|
+
- Full SEO rendering pipeline
|
|
51
|
+
|
|
52
|
+
### E2E Tests (5%)
|
|
53
|
+
- Gem installation in dummy Rails app
|
|
54
|
+
- Full workflow da generator a HTML output
|
|
55
|
+
- Rake tasks end-to-end
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Setup Testing Environment
|
|
60
|
+
|
|
61
|
+
### spec/spec_helper.rb
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# frozen_string_literal: true
|
|
65
|
+
|
|
66
|
+
require "simplecov"
|
|
67
|
+
SimpleCov.start "rails" do
|
|
68
|
+
add_filter "/spec/"
|
|
69
|
+
add_filter "/vendor/"
|
|
70
|
+
|
|
71
|
+
add_group "DSL", "lib/better_seo/dsl"
|
|
72
|
+
add_group "Generators", "lib/better_seo/generators"
|
|
73
|
+
add_group "Validators", "lib/better_seo/validators"
|
|
74
|
+
add_group "Rails", "lib/better_seo/rails"
|
|
75
|
+
add_group "Images", "lib/better_seo/images"
|
|
76
|
+
|
|
77
|
+
minimum_coverage 90
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
require "better_seo"
|
|
81
|
+
require "rspec"
|
|
82
|
+
require "webmock/rspec"
|
|
83
|
+
require "vcr"
|
|
84
|
+
|
|
85
|
+
# Disable external HTTP requests
|
|
86
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
|
87
|
+
|
|
88
|
+
# VCR configuration
|
|
89
|
+
VCR.configure do |config|
|
|
90
|
+
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
|
91
|
+
config.hook_into :webmock
|
|
92
|
+
config.configure_rspec_metadata!
|
|
93
|
+
config.default_cassette_options = { record: :new_episodes }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
RSpec.configure do |config|
|
|
97
|
+
# Enable flags like --only-failures and --next-failure
|
|
98
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
99
|
+
|
|
100
|
+
# Disable RSpec exposing methods globally
|
|
101
|
+
config.disable_monkey_patching!
|
|
102
|
+
|
|
103
|
+
# Use expect syntax
|
|
104
|
+
config.expect_with :rspec do |c|
|
|
105
|
+
c.syntax = :expect
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Reset BetterSeo configuration before each test
|
|
109
|
+
config.before(:each) do
|
|
110
|
+
BetterSeo.reset_configuration!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Random order
|
|
114
|
+
config.order = :random
|
|
115
|
+
Kernel.srand config.seed
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### spec/rails_helper.rb
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# frozen_string_literal: true
|
|
123
|
+
|
|
124
|
+
require "spec_helper"
|
|
125
|
+
|
|
126
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
127
|
+
|
|
128
|
+
# Dummy Rails app per testing
|
|
129
|
+
require File.expand_path("../dummy/config/environment", __FILE__)
|
|
130
|
+
|
|
131
|
+
require "rspec/rails"
|
|
132
|
+
require "factory_bot_rails"
|
|
133
|
+
|
|
134
|
+
RSpec.configure do |config|
|
|
135
|
+
config.use_transactional_fixtures = true
|
|
136
|
+
config.infer_spec_type_from_file_location!
|
|
137
|
+
config.filter_rails_from_backtrace!
|
|
138
|
+
|
|
139
|
+
# FactoryBot
|
|
140
|
+
config.include FactoryBot::Syntax::Methods
|
|
141
|
+
|
|
142
|
+
# Rails helpers
|
|
143
|
+
config.include ActionView::Helpers::TagHelper, type: :helper
|
|
144
|
+
config.include ActionView::Context, type: :helper
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Testing Patterns
|
|
151
|
+
|
|
152
|
+
### 1. Unit Test - DSL Classes
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# spec/dsl/meta_tags_spec.rb
|
|
156
|
+
RSpec.describe BetterSeo::DSL::MetaTags do
|
|
157
|
+
subject(:meta) { described_class.new }
|
|
158
|
+
|
|
159
|
+
describe "#title" do
|
|
160
|
+
it "sets title" do
|
|
161
|
+
meta.title "My Title"
|
|
162
|
+
expect(meta.title).to eq("My Title")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "returns self for chaining" do
|
|
166
|
+
expect(meta.title("Title")).to eq(meta)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context "when title too long" do
|
|
170
|
+
it "raises validation error on build" do
|
|
171
|
+
meta.title "A" * 80
|
|
172
|
+
expect { meta.build }.to raise_error(BetterSeo::ValidationError)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe "#build" do
|
|
178
|
+
it "returns configuration hash" do
|
|
179
|
+
meta.title "Title"
|
|
180
|
+
meta.description "Description"
|
|
181
|
+
|
|
182
|
+
result = meta.build
|
|
183
|
+
|
|
184
|
+
expect(result).to eq({
|
|
185
|
+
title: "Title",
|
|
186
|
+
description: "Description"
|
|
187
|
+
})
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "validates configuration" do
|
|
191
|
+
# Validation logic tested
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 2. Unit Test - Generators
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# spec/generators/meta_tags_generator_spec.rb
|
|
201
|
+
RSpec.describe BetterSeo::Generators::MetaTagsGenerator do
|
|
202
|
+
let(:config) do
|
|
203
|
+
{
|
|
204
|
+
charset: "UTF-8",
|
|
205
|
+
title: "Test Page",
|
|
206
|
+
description: "Test description"
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
subject(:generator) { described_class.new(config) }
|
|
211
|
+
|
|
212
|
+
describe "#generate" do
|
|
213
|
+
subject(:html) { generator.generate }
|
|
214
|
+
|
|
215
|
+
it "generates charset tag" do
|
|
216
|
+
expect(html).to include('<meta charset="UTF-8">')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "generates title tag" do
|
|
220
|
+
expect(html).to include('<title>Test Page</title>')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "generates description tag" do
|
|
224
|
+
expect(html).to include('<meta name="description" content="Test description">')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
context "with XSS attempt" do
|
|
228
|
+
let(:config) { { title: '<script>alert("xss")</script>' } }
|
|
229
|
+
|
|
230
|
+
it "escapes HTML entities" do
|
|
231
|
+
expect(html).to include('<script>')
|
|
232
|
+
expect(html).not_to include('<script>')
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
context "with unicode characters" do
|
|
237
|
+
let(:config) { { title: "Titolo con àccénti é ümläut" } }
|
|
238
|
+
|
|
239
|
+
it "preserves unicode" do
|
|
240
|
+
expect(html).to include("àccénti é ümläut")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 3. Integration Test - Configuration Loading
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# spec/integration/configuration_spec.rb
|
|
251
|
+
RSpec.describe "Configuration Loading", type: :integration do
|
|
252
|
+
let(:yaml_content) do
|
|
253
|
+
<<~YAML
|
|
254
|
+
production:
|
|
255
|
+
site_name: "Production Site"
|
|
256
|
+
meta_tags:
|
|
257
|
+
default_title: "Prod Title"
|
|
258
|
+
sitemap:
|
|
259
|
+
enabled: true
|
|
260
|
+
host: "https://prod.example.com"
|
|
261
|
+
YAML
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
let(:config_path) { "tmp/better_seo_test.yml" }
|
|
265
|
+
|
|
266
|
+
before do
|
|
267
|
+
File.write(config_path, yaml_content)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
after do
|
|
271
|
+
File.delete(config_path) if File.exist?(config_path)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "loads configuration from YAML file" do
|
|
275
|
+
BetterSeo.configure do |config|
|
|
276
|
+
config.load_from_file(config_path, environment: :production)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
expect(BetterSeo.configuration.site_name).to eq("Production Site")
|
|
280
|
+
expect(BetterSeo.configuration.meta_tags.default_title).to eq("Prod Title")
|
|
281
|
+
expect(BetterSeo.configuration.sitemap.host).to eq("https://prod.example.com")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it "validates after loading" do
|
|
285
|
+
expect {
|
|
286
|
+
BetterSeo.configure do |config|
|
|
287
|
+
config.load_from_file(config_path, environment: :production)
|
|
288
|
+
end
|
|
289
|
+
}.not_to raise_error
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 4. Integration Test - Rails Helpers
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# spec/helpers/meta_tags_helper_spec.rb
|
|
298
|
+
RSpec.describe BetterSeo::Rails::Helpers::MetaTagsHelper, type: :helper do
|
|
299
|
+
before do
|
|
300
|
+
BetterSeo.configure do |config|
|
|
301
|
+
config.meta_tags.default_title = "Default Title"
|
|
302
|
+
config.meta_tags.default_description = "Default Description"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
describe "#seo_meta_tags" do
|
|
307
|
+
it "renders meta tags with defaults" do
|
|
308
|
+
html = helper.seo_meta_tags
|
|
309
|
+
|
|
310
|
+
expect(html).to include('<title>Default Title</title>')
|
|
311
|
+
expect(html).to include('name="description" content="Default Description"')
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "accepts block for overrides" do
|
|
315
|
+
html = helper.seo_meta_tags do |meta|
|
|
316
|
+
meta.title "Custom Title"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
expect(html).to include('<title>Custom Title</title>')
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "accepts hash options" do
|
|
323
|
+
html = helper.seo_meta_tags(title: "Hash Title")
|
|
324
|
+
|
|
325
|
+
expect(html).to include('<title>Hash Title</title>')
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "returns html_safe string" do
|
|
329
|
+
html = helper.seo_meta_tags
|
|
330
|
+
expect(html).to be_html_safe
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
describe "#render_seo_tags" do
|
|
335
|
+
it "renders all SEO tags when enabled" do
|
|
336
|
+
BetterSeo.configuration.open_graph.enabled = true
|
|
337
|
+
BetterSeo.configuration.twitter.enabled = true
|
|
338
|
+
|
|
339
|
+
html = helper.render_seo_tags
|
|
340
|
+
|
|
341
|
+
expect(html).to include('<title>') # meta tags
|
|
342
|
+
expect(html).to include('property="og:') # open graph
|
|
343
|
+
expect(html).to include('name="twitter:') # twitter cards
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 5. E2E Test - Full Workflow
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
# spec/e2e/full_seo_workflow_spec.rb
|
|
353
|
+
RSpec.describe "Full SEO Workflow", type: :feature do
|
|
354
|
+
it "generates complete SEO markup from configuration" do
|
|
355
|
+
# 1. Configure
|
|
356
|
+
BetterSeo.configure do |config|
|
|
357
|
+
config.site_name = "Test Site"
|
|
358
|
+
|
|
359
|
+
config.meta_tags do
|
|
360
|
+
default_title "Test Title"
|
|
361
|
+
default_description "Test Description"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
config.open_graph do
|
|
365
|
+
site_name "Test Site"
|
|
366
|
+
default_type "website"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# 2. Build DSL
|
|
371
|
+
meta_dsl = BetterSeo::DSL::MetaTags.new
|
|
372
|
+
meta_dsl.title "Article Title"
|
|
373
|
+
meta_dsl.description "Article Description"
|
|
374
|
+
|
|
375
|
+
og_dsl = BetterSeo::DSL::OpenGraph.new
|
|
376
|
+
og_dsl.title "Article Title"
|
|
377
|
+
og_dsl.type "article"
|
|
378
|
+
|
|
379
|
+
# 3. Generate HTML
|
|
380
|
+
meta_html = BetterSeo::Generators::MetaTagsGenerator.new(meta_dsl.build).generate
|
|
381
|
+
og_html = BetterSeo::Generators::OpenGraphGenerator.new(og_dsl.build).generate
|
|
382
|
+
|
|
383
|
+
# 4. Verify output
|
|
384
|
+
expect(meta_html).to include('<title>Article Title</title>')
|
|
385
|
+
expect(og_html).to include('property="og:title" content="Article Title"')
|
|
386
|
+
expect(og_html).to include('property="og:type" content="article"')
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Fixtures e Factories
|
|
394
|
+
|
|
395
|
+
### FactoryBot Factories
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# spec/factories/articles.rb
|
|
399
|
+
FactoryBot.define do
|
|
400
|
+
factory :article do
|
|
401
|
+
title { "Test Article" }
|
|
402
|
+
description { "Test Description" }
|
|
403
|
+
published_at { Time.current }
|
|
404
|
+
|
|
405
|
+
trait :with_seo do
|
|
406
|
+
seo_title { "SEO Title" }
|
|
407
|
+
seo_description { "SEO Description" }
|
|
408
|
+
seo_keywords { ["ruby", "seo"] }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
trait :published do
|
|
412
|
+
published { true }
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Fixtures Files
|
|
419
|
+
|
|
420
|
+
```yaml
|
|
421
|
+
# spec/fixtures/config/better_seo.yml
|
|
422
|
+
test:
|
|
423
|
+
site_name: "Test Site"
|
|
424
|
+
default_locale: en
|
|
425
|
+
available_locales:
|
|
426
|
+
- en
|
|
427
|
+
- it
|
|
428
|
+
|
|
429
|
+
meta_tags:
|
|
430
|
+
default_title: "Test Title"
|
|
431
|
+
default_description: "Test Description"
|
|
432
|
+
|
|
433
|
+
sitemap:
|
|
434
|
+
enabled: false
|
|
435
|
+
|
|
436
|
+
robots:
|
|
437
|
+
enabled: false
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Shared Examples
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
# spec/support/shared_examples/dsl_examples.rb
|
|
446
|
+
RSpec.shared_examples "a DSL builder" do
|
|
447
|
+
it "responds to #build" do
|
|
448
|
+
expect(subject).to respond_to(:build)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it "responds to #to_h" do
|
|
452
|
+
expect(subject).to respond_to(:to_h)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it "responds to #merge!" do
|
|
456
|
+
expect(subject).to respond_to(:merge!)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "returns self for method chaining" do
|
|
460
|
+
# Assume first_method exists
|
|
461
|
+
expect(subject.send(described_class.instance_methods(false).first, "value")).to eq(subject)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Usage:
|
|
466
|
+
RSpec.describe BetterSeo::DSL::MetaTags do
|
|
467
|
+
it_behaves_like "a DSL builder"
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Custom Matchers
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# spec/support/matchers/html_matchers.rb
|
|
477
|
+
RSpec::Matchers.define :have_meta_tag do |name, content|
|
|
478
|
+
match do |html|
|
|
479
|
+
html.include?(%(<meta name="#{name}" content="#{content}">))
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
failure_message do |html|
|
|
483
|
+
"expected HTML to include meta tag '#{name}' with content '#{content}', but got:\n#{html}"
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Usage:
|
|
488
|
+
expect(html).to have_meta_tag("description", "My Description")
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Testing Images (Step 07)
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
# spec/images/converter_spec.rb
|
|
497
|
+
RSpec.describe BetterSeo::Images::Converter do
|
|
498
|
+
let(:source_path) { "spec/fixtures/images/test.jpg" }
|
|
499
|
+
let(:output_path) { "tmp/test.webp" }
|
|
500
|
+
|
|
501
|
+
after do
|
|
502
|
+
File.delete(output_path) if File.exist?(output_path)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
describe ".to_webp" do
|
|
506
|
+
it "converts JPEG to WebP", :vips do
|
|
507
|
+
result = described_class.to_webp(source_path, output_path)
|
|
508
|
+
|
|
509
|
+
expect(File.exist?(result)).to be true
|
|
510
|
+
expect(result).to end_with(".webp")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
it "reduces file size" do
|
|
514
|
+
original_size = File.size(source_path)
|
|
515
|
+
|
|
516
|
+
described_class.to_webp(source_path, output_path, quality: 80)
|
|
517
|
+
|
|
518
|
+
webp_size = File.size(output_path)
|
|
519
|
+
expect(webp_size).to be < original_size
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Performance Testing
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
# spec/performance/generators_performance_spec.rb
|
|
531
|
+
RSpec.describe "Generators Performance" do
|
|
532
|
+
it "generates meta tags in < 1ms" do
|
|
533
|
+
config = { title: "Title", description: "Description" }
|
|
534
|
+
generator = BetterSeo::Generators::MetaTagsGenerator.new(config)
|
|
535
|
+
|
|
536
|
+
time = Benchmark.realtime do
|
|
537
|
+
1000.times { generator.generate }
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
expect(time / 1000).to be < 0.001 # < 1ms per generation
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
it "generates sitemap for 10k URLs in < 1s" do
|
|
544
|
+
urls = 10_000.times.map do |i|
|
|
545
|
+
{ loc: "https://example.com/page-#{i}", priority: 0.5 }
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
generator = BetterSeo::Generators::SitemapGenerator.new(urls)
|
|
549
|
+
|
|
550
|
+
time = Benchmark.realtime { generator.generate }
|
|
551
|
+
|
|
552
|
+
expect(time).to be < 1.0 # < 1 second
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## CI/CD Integration
|
|
560
|
+
|
|
561
|
+
### GitHub Actions
|
|
562
|
+
|
|
563
|
+
```yaml
|
|
564
|
+
# .github/workflows/ci.yml
|
|
565
|
+
name: CI
|
|
566
|
+
|
|
567
|
+
on: [push, pull_request]
|
|
568
|
+
|
|
569
|
+
jobs:
|
|
570
|
+
test:
|
|
571
|
+
runs-on: ubuntu-latest
|
|
572
|
+
|
|
573
|
+
strategy:
|
|
574
|
+
matrix:
|
|
575
|
+
ruby: ['3.0', '3.1', '3.2', '3.3']
|
|
576
|
+
rails: ['6.1', '7.0', '7.1']
|
|
577
|
+
|
|
578
|
+
steps:
|
|
579
|
+
- uses: actions/checkout@v3
|
|
580
|
+
|
|
581
|
+
- name: Set up Ruby
|
|
582
|
+
uses: ruby/setup-ruby@v1
|
|
583
|
+
with:
|
|
584
|
+
ruby-version: ${{ matrix.ruby }}
|
|
585
|
+
bundler-cache: true
|
|
586
|
+
|
|
587
|
+
- name: Install libvips
|
|
588
|
+
run: sudo apt-get install libvips-dev
|
|
589
|
+
|
|
590
|
+
- name: Run tests
|
|
591
|
+
run: bundle exec rspec
|
|
592
|
+
|
|
593
|
+
- name: Upload coverage
|
|
594
|
+
uses: codecov/codecov-action@v3
|
|
595
|
+
with:
|
|
596
|
+
files: ./coverage/.resultset.json
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Coverage Reports
|
|
602
|
+
|
|
603
|
+
### SimpleCov Configuration
|
|
604
|
+
|
|
605
|
+
```ruby
|
|
606
|
+
# spec/spec_helper.rb
|
|
607
|
+
SimpleCov.start "rails" do
|
|
608
|
+
add_filter "/spec/"
|
|
609
|
+
add_filter "/vendor/"
|
|
610
|
+
|
|
611
|
+
add_group "DSL", "lib/better_seo/dsl"
|
|
612
|
+
add_group "Generators", "lib/better_seo/generators"
|
|
613
|
+
add_group "Validators", "lib/better_seo/validators"
|
|
614
|
+
add_group "Rails", "lib/better_seo/rails"
|
|
615
|
+
add_group "Images", "lib/better_seo/images"
|
|
616
|
+
|
|
617
|
+
minimum_coverage 90
|
|
618
|
+
refuse_coverage_drop
|
|
619
|
+
|
|
620
|
+
# Formatters
|
|
621
|
+
formatter SimpleCov::Formatter::MultiFormatter.new([
|
|
622
|
+
SimpleCov::Formatter::HTMLFormatter,
|
|
623
|
+
SimpleCov::Formatter::Console
|
|
624
|
+
])
|
|
625
|
+
end
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Viewing Coverage
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
# Run tests
|
|
632
|
+
bundle exec rspec
|
|
633
|
+
|
|
634
|
+
# Open coverage report
|
|
635
|
+
open coverage/index.html
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Best Practices
|
|
641
|
+
|
|
642
|
+
### ✅ DO
|
|
643
|
+
|
|
644
|
+
- Write tests BEFORE implementation (TDD)
|
|
645
|
+
- Use descriptive test names
|
|
646
|
+
- Test edge cases and error conditions
|
|
647
|
+
- Keep tests isolated (no shared state)
|
|
648
|
+
- Use factories for complex objects
|
|
649
|
+
- Mock external dependencies (HTTP, file system quando possibile)
|
|
650
|
+
- Test both happy path and sad path
|
|
651
|
+
|
|
652
|
+
### ❌ DON'T
|
|
653
|
+
|
|
654
|
+
- Don't test framework code (Rails, RSpec)
|
|
655
|
+
- Don't write flaky tests
|
|
656
|
+
- Don't share state between tests
|
|
657
|
+
- Don't use sleep in tests
|
|
658
|
+
- Don't skip tests (fix or delete)
|
|
659
|
+
- Don't test implementation details
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
**Ultima modifica**: 2025-10-22
|