diff --git a/CHANGELOG.md b/CHANGELOG.md index bda05ed2..e0a459de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 517a8a89..7c9c5f3e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/examples/openapi.yaml b/examples/openapi.yaml index 22a33442..d49713d4 100644 --- a/examples/openapi.yaml +++ b/examples/openapi.yaml @@ -26,3 +26,5 @@ paths: properties: hello: type: string + "401": + description: Unauthorized diff --git a/lib/openapi_first/test.rb b/lib/openapi_first/test.rb index 49b96338..d6ab74e4 100644 --- a/lib/openapi_first/test.rb +++ b/lib/openapi_first/test.rb @@ -18,6 +18,7 @@ def initialize @minimum_coverage = 0 @coverage_formatter = Coverage::TerminalFormatter @coverage_formatter_options = {} + @skip_response_coverage = nil yield self end @@ -25,10 +26,14 @@ 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 @@ -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, diff --git a/lib/openapi_first/test/coverage.rb b/lib/openapi_first/test/coverage.rb index 2d7b9bcc..935b7a6d 100644 --- a/lib/openapi_first/test/coverage.rb +++ b/lib/openapi_first/test/coverage.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/openapi_first/test/coverage/plan.rb b/lib/openapi_first/test/coverage/plan.rb index 8507ec0d..a1b9c85c 100644 --- a/lib/openapi_first/test/coverage/plan.rb +++ b/lib/openapi_first/test/coverage/plan.rb @@ -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) @@ -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) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1950090a..6fb61a5f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/test/coverage/plan_spec.rb b/spec/test/coverage/plan_spec.rb index bd48e1ee..c495ae3e 100644 --- a/spec/test/coverage/plan_spec.rb +++ b/spec/test/coverage/plan_spec.rb @@ -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 @@ -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 diff --git a/spec/test/coverage_spec.rb b/spec/test/coverage_spec.rb index 484cf183..80e88b5f 100644 --- a/spec/test/coverage_spec.rb +++ b/spec/test/coverage_spec.rb @@ -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 @@ -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 diff --git a/spec/test_spec.rb b/spec/test_spec.rb index c9c59c56..412d88db 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -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 @@ -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