Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- OpenapiFirst::Test.report_coverage now includes fractional digits when returning a coverage value to avoid reporting "0% / no requests made" even though some requests have been made.
- Add option to skip certain responses in coverage calculation

## 2.4.0

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ Here is how to set it up for RSpec in your `spec/spec_helper.rb`:
1. Register all OpenAPI documents to track coverage for and start tracking. This should go at the top of you test helper file before loading application code.
```ruby
require 'openapi_first'
OpenapiFirst::Test.setup do |test|
OpenapiFirst::Test.setup do |s|
test.register('openapi/openapi.yaml')
test.minimum_coverage = 100 # Setting this will lead to an `exit 2` if coverage is below minimum
test.skip_response_coverage { it.status == '500' }
end
```
2. Wrap your app with silent request / response validation. This validates all requets/responses you do during your test run. (✷1)
Expand Down
2 changes: 2 additions & 0 deletions examples/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ paths:
properties:
hello:
type: string
"401":
description: Unauthorized
12 changes: 9 additions & 3 deletions lib/openapi_first/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ def initialize
@minimum_coverage = 0
@coverage_formatter = Coverage::TerminalFormatter
@coverage_formatter_options = {}
@skip_response_coverage = nil
yield self
end

def register(*)
Test.register(*)
end

def skip_response_coverage_for_status(*statuses); end

attr_accessor :minimum_coverage, :coverage_formatter_options, :coverage_formatter

def skip_response_coverage(&block)
return @skip_response_coverage unless block_given?

@skip_response_coverage = block
end

# This called at_exit
def handle_exit
coverage = Coverage.result.coverage
Expand All @@ -54,8 +59,9 @@ def self.setup(&)
raise ArgumentError, "Please provide a block to #{self.class}.setup to register you API descriptions"
end

Coverage.start
Coverage.install
setup = Setup.new(&)
Coverage.start(skip_response: setup.skip_response_coverage)

if definitions.empty?
raise NotRegisteredError,
Expand Down
31 changes: 19 additions & 12 deletions lib/openapi_first/test/coverage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ module Coverage
Result = Data.define(:plans, :coverage)

class << self
def start
attr_reader :current_run

def install
return if @installed

@after_request_validation = lambda do |validated_request, oad|
track_request(validated_request, oad)
end
Expand All @@ -26,12 +30,21 @@ def start
config.after_request_validation(&@after_request_validation)
config.after_response_validation(&@after_response_validation)
end
@installed = true
end

def start(skip_response: nil)
@current_run = Test.definitions.values.to_h do |oad|
plan = Plan.for(oad, skip_response:)
[oad.filepath, plan]
end
end

def stop
def uninstall
configuration = OpenapiFirst.configuration
configuration.hooks[:after_request_validation].delete(@after_request_validation)
configuration.hooks[:after_response_validation].delete(@after_response_validation)
@installed = nil
end

# Clear current coverage run
Expand All @@ -51,24 +64,18 @@ def result
Result.new(plans:, coverage:)
end

private

# Returns all plans (Plan) that were registered for this run
def plans
current_run.values
current_run&.values
end

private

def coverage
return 0 if plans.empty?
return 0 unless plans

plans.sum(&:coverage) / plans.length
end

def current_run
@current_run ||= Test.definitions.values.to_h do |oad|
[oad.filepath, Plan.new(oad)]
end
end
end
end
end
Expand Down
27 changes: 15 additions & 12 deletions lib/openapi_first/test/coverage/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,25 @@ module Coverage
class Plan
class UnknownRequestError < StandardError; end

def initialize(oad)
@oad = oad
@routes = []
@index = {}
@filepath = oad.filepath
def self.for(oad, skip_response: nil)
plan = new(filepath: oad.filepath)
oad.routes.each do |route|
add_route request_method: route.request_method,
path: route.path,
requests: route.requests,
responses: route.responses
responses = skip_response ? route.responses.reject(&skip_response) : route.responses
plan.add_route request_method: route.request_method,
path: route.path,
requests: route.requests,
responses:
end
plan
end

attr_reader :filepath, :oad, :routes
def initialize(filepath:)
@routes = []
@index = {}
@filepath = filepath
end

attr_reader :filepath, :routes
private attr_reader :index

def track_request(validated_request)
Expand All @@ -52,8 +57,6 @@ def tasks
index.values
end

private

def add_route(request_method:, path:, requests:, responses:)
request_tasks = requests.to_a.map do |request|
index[request.key] = RequestTask.new(request)
Expand Down
3 changes: 2 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
c.syntax = :expect
end

config.after do
config.after(:each) do
OpenapiFirst::Test.definitions.clear
OpenapiFirst::Test::Coverage.uninstall
end
end
18 changes: 17 additions & 1 deletion spec/test/coverage/plan_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
oad.validate_response(request, response)
end

subject(:plan) { described_class.new(oad) }
subject(:plan) { described_class.for(oad) }

it 'tracks requests and responses' do
request = plan.routes.first.requests.first
Expand Down Expand Up @@ -137,4 +137,20 @@
expect(plan.tasks[1].status).to eq('200')
expect(plan.tasks[2].status).to eq('4XX')
end

context 'with skip_response option' do
let(:plan) do
skip_response = ->(response) { response.status == '4XX' }
described_class.for(oad, skip_response:)
end

it 'can be done without the skipped response' do
expect(plan).not_to be_done

plan.track_request(valid_request)
plan.track_response(valid_response)

expect(plan.coverage).to eq(100)
end
end
end
21 changes: 13 additions & 8 deletions spec/test/coverage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
let(:definition) { OpenapiFirst.load(filepath) }

before(:each) do
described_class.install
OpenapiFirst::Test.register(filepath)
described_class.start
end

after(:each) do
described_class.stop
described_class.uninstall
described_class.reset
end

Expand All @@ -22,18 +23,22 @@

let(:result) { described_class.result }

describe '.start' do
after { described_class.stop }

describe '.install' do
it 'installs global hooks' do
described_class.install

hooks = OpenapiFirst.configuration.hooks
described_class.stop
expect(hooks[:after_request_validation]).to be_empty
expect(hooks[:after_response_validation]).to be_empty
described_class.start
expect(hooks[:after_request_validation]).not_to be_empty
expect(hooks[:after_response_validation]).not_to be_empty
end

it 'does not install hooks multiple times' do
2.times { described_class.install }

hooks = OpenapiFirst.configuration.hooks
expect(hooks[:after_request_validation].count).to eq(1)
expect(hooks[:after_response_validation].count).to eq(1)
end
end

describe '.result' do
Expand Down
10 changes: 9 additions & 1 deletion spec/test_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
RSpec.describe OpenapiFirst::Test do
after(:each) do
described_class.definitions.clear
OpenapiFirst::Test::Coverage.stop
OpenapiFirst::Test::Coverage.uninstall
OpenapiFirst::Test::Coverage.reset
end

Expand Down Expand Up @@ -51,6 +51,14 @@
expect(described_class.definitions[:default].filepath).to eq(OpenapiFirst.load('./examples/openapi.yaml').filepath)
end

it 'can skip responses for coverage' do
described_class.setup do |test|
test.register('./examples/openapi.yaml')
test.skip_response_coverage { |res| res.status == '401' }
end
expect(described_class::Coverage.plans.first.tasks.count).to eq(2)
end

it 'raises an error if no block is given' do
expect do
described_class.setup
Expand Down