├── .rspec ├── .gitignore ├── Gemfile ├── spec ├── fixtures │ └── import_stubs.mimic ├── spec_helper.rb └── fake_host_spec.rb ├── .travis.yml ├── features ├── support │ ├── mimic_runner.rb │ ├── hash_key_path.rb │ ├── env.rb │ └── http_client.rb ├── steps │ ├── logging_steps.rb │ ├── shell_steps.rb │ ├── mimic_steps.rb │ └── http_client_steps.rb ├── logging_requests.feature ├── resetting_stubs.feature ├── using_rack_middlewares.feature ├── stubbing_requests_by_method.feature ├── stubbing_requests_from_a_file.feature ├── stubbing_requests_with_parameters.feature ├── checking_requests_were_made.feature ├── stubbing_requests_by_path.feature ├── echoing_request_in_response.feature └── configuring_mimic_via_http.feature ├── examples └── mimicd ├── CHANGES ├── LICENSE ├── Gemfile.lock ├── mimic.gemspec ├── lib ├── mimic.rb └── mimic │ ├── api.rb │ └── fake_host.rb ├── Rakefile └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | rdoc -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gem "rake" 3 | gem "rdoc" 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/fixtures/import_stubs.mimic: -------------------------------------------------------------------------------- 1 | get "/imported/path" do 2 | [200, {}, ""] 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.6 6 | - 2.2.3 7 | script: bundle exec rake all 8 | -------------------------------------------------------------------------------- /features/support/mimic_runner.rb: -------------------------------------------------------------------------------- 1 | require 'mimic' 2 | 3 | class MimicRunner 4 | def evaluate(code_string) 5 | instance_eval(code_string) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /features/steps/logging_steps.rb: -------------------------------------------------------------------------------- 1 | Then /^I should see "([^"]*)" written to STDOUT$/ do |output| 2 | expect(TEST_STDOUT.tap { |io| io.rewind }.read).to include(output) 3 | end 4 | -------------------------------------------------------------------------------- /features/support/hash_key_path.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def value_for_key_path(key_path_string) 3 | key_path_string.split(".").inject(self) do |result, key| 4 | result[key] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'mocha' 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[.. lib])) 5 | require 'mimic' 6 | require 'rspec/expectations' 7 | 8 | RSpec.configure do |config| 9 | config.mock_with :mocha 10 | end 11 | -------------------------------------------------------------------------------- /features/steps/shell_steps.rb: -------------------------------------------------------------------------------- 1 | TEMP_FILES = [] 2 | 3 | Given /^the file "([^\"]*)" exists with the contents:$/ do |file_path, string| 4 | File.open(file_path, "w") { |io| io.write(string) } 5 | TEMP_FILES << file_path 6 | end 7 | 8 | After do 9 | TEMP_FILES.each { |path| FileUtils.rm(path) if File.exist?(path) } 10 | end 11 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[.. .. lib])) 2 | 3 | TEST_STDOUT = StringIO.new 4 | 5 | Before do 6 | if test_proxy = ENV["MIMIC_TEST_PROXY"] 7 | HttpClient.use_proxy(test_proxy) 8 | end 9 | 10 | $stdout = TEST_STDOUT 11 | end 12 | 13 | After do 14 | $stdout = STDOUT 15 | end 16 | -------------------------------------------------------------------------------- /examples/mimicd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 4 | 5 | require 'mimic' 6 | 7 | Daemons.run_proc("mimic") do 8 | Mimic.mimic(:fork => false, :remote_configuration_path => "/api") do 9 | get("/ping") { "pong\n" } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /features/logging_requests.feature: -------------------------------------------------------------------------------- 1 | Feature: Logging incoming requests 2 | In order to check that Mimic is working properly or provide further debug information 3 | As a developer 4 | I want to be able to tell Mimic to log incoming requests 5 | 6 | Scenario: Logging to STDOUT 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988, :log => $stdout).get("/some/path") 10 | """ 11 | When I make an HTTP GET request to "http://localhost:11988/some/path" 12 | Then I should see "GET /some/path HTTP/1.1" written to STDOUT 13 | -------------------------------------------------------------------------------- /features/resetting_stubs.feature: -------------------------------------------------------------------------------- 1 | Feature: Resetting stubs 2 | In order to create a deterministic clean slate at the beginning of my specs 3 | As a developer 4 | I want to be able to reset all previously configured request stubs 5 | 6 | Scenario: Clearing a stubbed request 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988).get("/some/path").returning("Hello World", 201) 10 | """ 11 | When I evaluate the code: 12 | """ 13 | Mimic.reset_all! 14 | """ 15 | And I make an HTTP GET request to "http://localhost:11988/some/path" 16 | Then I should receive an HTTP 404 response with an empty body 17 | 18 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | ## 0.4.3 2 | 3 | * Added request logging support. 4 | 5 | ## 0.4.2 6 | 7 | * Remove unnecessary signal traps. 8 | 9 | ## 0.4.1 10 | 11 | * Fixed bug where stubs wouldn't actually be cleared correctly. 12 | 13 | ## 0.4.0 14 | 15 | * Mimic can now be run in it's own process and configured externally using a REST API. 16 | * Mimic can be run using the Daemons gem safely (pass :fork => false to disable built-in forking). 17 | * All existing stubs can be cleared. 18 | 19 | ## 0.3.0 20 | 21 | * All verb methods (get, post etc.) can take blocks 22 | 23 | ## 0.2.0 24 | 25 | * Added support for using Rack middleware 26 | * Removed host file modification feature 27 | * Refactor code top build on top of Sinatra::Base 28 | 29 | ## 0.1.0 30 | 31 | * Initial release 32 | -------------------------------------------------------------------------------- /features/steps/mimic_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I have a mimic specification with:$/ do |string| 2 | MimicRunner.new.evaluate(string) 3 | end 4 | 5 | Given /^that Mimic is running and accepting remote configuration on "([^\"]*)"$/ do |api_endpoint| 6 | Mimic.mimic(:port => 11988, :remote_configuration_path => api_endpoint, :wait_timeout => 10, :fork => true) 7 | end 8 | 9 | Given /^that Mimic is running and accepting remote configuration on "([^\"]*)" with the existing stubs:$/ do |api_endpoint, existing_stubs| 10 | Mimic.mimic(:port => 11988, :remote_configuration_path => api_endpoint, :wait_timeout => 10, :fork => true) do 11 | eval(existing_stubs) 12 | end 13 | end 14 | 15 | When /^I evaluate the code:$/ do |string| 16 | eval(string) 17 | end 18 | 19 | After do 20 | Mimic.cleanup! 21 | 22 | # wait for Mimic to shutdown 23 | start_time, timeout = Time.now, 3 24 | 25 | until !Mimic::Server.instance.listening?('localhost', 11988) 26 | if Time.now > (start_time + timeout) 27 | raise "Socket did not close within #{timeout} seconds" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Luke Redpath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /features/using_rack_middlewares.feature: -------------------------------------------------------------------------------- 1 | Feature: Injecting Rack middleware into the request chain for a stub 2 | In order to test common scenarios (e.g. authentication) 3 | As a developer 4 | I want to be able to use Rack middlewares for certain responses 5 | 6 | Scenario: Using Rack::Auth to simulate failed authentication 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988) do 10 | use Rack::Auth::Basic do |username, password| 11 | end 12 | 13 | get("/some/path") 14 | end 15 | """ 16 | When I make an HTTP GET request to "http://localhost:11988/some/path" 17 | Then I should receive an HTTP 401 response 18 | 19 | Scenario: Using Rack::Auth to simulate successful authentication 20 | Given I have a mimic specification with: 21 | """ 22 | Mimic.mimic(:port => 11988) do 23 | use Rack::Auth::Basic do |username, password| 24 | username == 'test' && password == 'pass' 25 | end 26 | 27 | get("/some/path") 28 | end 29 | """ 30 | When I make an HTTP GET request to "http://test:pass@localhost:11988/some/path" 31 | Then I should receive an HTTP 200 response -------------------------------------------------------------------------------- /features/stubbing_requests_by_method.feature: -------------------------------------------------------------------------------- 1 | Feature: Stubbing requests by path 2 | In order to test a range of API endpoints and HTTP verbs 3 | As a developer 4 | I want to be able to stub requests to return specific responses depending on the request method 5 | 6 | Scenario: Stubbing a POST request to return a 201 response 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988).post("/some/path").returning("Hello World", 201) 10 | """ 11 | When I make an HTTP POST request to "http://localhost:11988/some/path" 12 | Then I should receive an HTTP 201 response with a body matching "Hello World" 13 | 14 | Scenario: Stubbing the same path with different responses for GET and POST 15 | Given I have a mimic specification with: 16 | """ 17 | Mimic.mimic(:port => 11988) do 18 | get("/some/path").returning("Some Record", 200) 19 | post("/some/path").returning("Created", 201) 20 | end 21 | """ 22 | When I make an HTTP GET request to "http://localhost:11988/some/path" 23 | Then I should receive an HTTP 200 response with a body matching "Some Record" 24 | When I make an HTTP POST request to "http://localhost:11988/some/path" 25 | Then I should receive an HTTP 201 response with a body matching "Created" 26 | -------------------------------------------------------------------------------- /features/stubbing_requests_from_a_file.feature: -------------------------------------------------------------------------------- 1 | Feature: Stubbing requests from a file 2 | In order to pre-load Mimic with a set of stubs 3 | As a developer 4 | I want to be able to store my stub configuration in a separate file and load it in at runtime 5 | 6 | Scenario: Stubbing requests using a file 7 | Given the file "/tmp/test.mimic" exists with the contents: 8 | """ 9 | get("/ping") { "pong" } 10 | """ 11 | And I have a mimic specification with: 12 | """ 13 | Mimic.mimic(:port => 11988) do 14 | import "/tmp/test.mimic" 15 | end 16 | """ 17 | When I make an HTTP GET request to "http://localhost:11988/ping" 18 | Then I should receive an HTTP 200 response with a body matching "pong" 19 | 20 | Scenario: Stubbed requests from a file persist even when Mimic is cleared 21 | Given the file "/tmp/test.mimic" exists with the contents: 22 | """ 23 | get("/ping") { "pong" } 24 | """ 25 | And I have a mimic specification with: 26 | """ 27 | Mimic.mimic(:port => 11988) do 28 | import "/tmp/test.mimic" 29 | end 30 | Mimic.reset_all! 31 | """ 32 | When I make an HTTP GET request to "http://localhost:11988/ping" 33 | Then I should receive an HTTP 200 response with a body matching "pong" 34 | -------------------------------------------------------------------------------- /features/stubbing_requests_with_parameters.feature: -------------------------------------------------------------------------------- 1 | Feature: Stubbing requests by path 2 | In order to test requests that use specific query parameters 3 | As a developer 4 | I want to be able to only stub requests that have the correct parameters 5 | 6 | Scenario: Accepting any parameters to a stubbed path 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988).get("/some/path") 10 | """ 11 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 12 | Then I should receive an HTTP 200 response with an empty body 13 | 14 | Scenario: Accepting specific parameters and matching 15 | Given I have a mimic specification with: 16 | """ 17 | Mimic.mimic(:port => 11988).get("/some/path").with_query_parameters("foo" => "bar") 18 | """ 19 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 20 | Then I should receive an HTTP 200 response with an empty body 21 | 22 | Scenario: Accepting specific parameters and matching 23 | Given I have a mimic specification with: 24 | """ 25 | Mimic.mimic(:port => 11988).get("/some/path").with_query_parameters("foo" => "bar") 26 | """ 27 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=baz" 28 | Then I should receive an HTTP 404 response 29 | -------------------------------------------------------------------------------- /features/support/http_client.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | 3 | class HttpClient 4 | attr_reader :last_response 5 | 6 | def self.use_proxy(proxy) 7 | RestClient.proxy = proxy 8 | end 9 | 10 | def initialize 11 | @last_response = nil 12 | end 13 | 14 | def perform_request(url, method, payload = nil, options={}) 15 | RestClient.send(method.downcase, url, options) do |response, request| 16 | @last_response = response 17 | end 18 | end 19 | 20 | def perform_request_with_payload(url, method, payload, options={}) 21 | RestClient.send(method.downcase, url, payload, options) do |response, request| 22 | @last_response = response 23 | end 24 | end 25 | 26 | def has_response_with_code_and_body?(status_code, response_body) 27 | if @last_response 28 | return @last_response.code.to_i == status_code && @last_response.to_s == response_body 29 | end 30 | end 31 | 32 | def has_response_with_code?(status_code) 33 | if @last_response 34 | @last_response.code.to_i == status_code 35 | end 36 | end 37 | 38 | def has_response_with_code_and_header?(status_code, header_key, header_value) 39 | if @last_response 40 | @last_response.code.to_i == status_code && 41 | @last_response.headers[beautify_header(header_key)] == header_value 42 | end 43 | end 44 | 45 | private 46 | 47 | def beautify_header(header_key) 48 | header_key.downcase.gsub(/-/, '_').to_sym 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /features/checking_requests_were_made.feature: -------------------------------------------------------------------------------- 1 | Feature: Checking requests were made 2 | In order to verify requests were made to mimic (like mock expectations) 3 | As a developer 4 | I want to be able to ask the Mimic API what requests were made 5 | 6 | Scenario: Configuring a request and verifying it was called 7 | Given that Mimic is running and accepting remote configuration on "/api" 8 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 9 | """ 10 | {"path": "/anything"} 11 | """ 12 | Then I should receive an HTTP 201 response with a body containing: 13 | """ 14 | {"stubs":["41f660b868e1308e8d87afbb84532b71"]} 15 | """ 16 | And I make an HTTP GET request to "http://localhost:11988/anything" 17 | And I make an HTTP GET request to "http://localhost:11988/api/requests" 18 | Then I should receive an HTTP 200 response with a body containing: 19 | """ 20 | {"requests":["41f660b868e1308e8d87afbb84532b71"]} 21 | """ 22 | 23 | Scenario: Configuring a request and verifying it was not called 24 | Given that Mimic is running and accepting remote configuration on "/api" 25 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 26 | """ 27 | {"path": "/anything"} 28 | """ 29 | Then I should receive an HTTP 201 response with a body containing: 30 | """ 31 | {"stubs":["41f660b868e1308e8d87afbb84532b71"]} 32 | """ 33 | And I make an HTTP GET request to "http://localhost:11988/api/requests" 34 | Then I should receive an HTTP 200 response with a body containing: 35 | """ 36 | {"requests":[]} 37 | """ 38 | 39 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mimic (0.4.3) 5 | json 6 | plist (~> 3.1.0) 7 | rack 8 | sinatra 9 | thin 10 | 11 | GEM 12 | remote: http://rubygems.org/ 13 | specs: 14 | builder (3.2.2) 15 | cucumber (2.3.3) 16 | builder (>= 2.1.2) 17 | cucumber-core (~> 1.4.0) 18 | cucumber-wire (~> 0.0.1) 19 | diff-lcs (>= 1.1.3) 20 | gherkin (~> 3.2.0) 21 | multi_json (>= 1.7.5, < 2.0) 22 | multi_test (>= 0.1.2) 23 | cucumber-core (1.4.0) 24 | gherkin (~> 3.2.0) 25 | cucumber-wire (0.0.1) 26 | daemons (1.2.3) 27 | diff-lcs (1.2.5) 28 | domain_name (0.5.20160310) 29 | unf (>= 0.0.5, < 1.0.0) 30 | eventmachine (1.2.0.1) 31 | gherkin (3.2.0) 32 | http-cookie (1.0.2) 33 | domain_name (~> 0.5) 34 | json (1.8.3) 35 | metaclass (0.0.4) 36 | mime-types (2.99.2) 37 | mocha (1.1.0) 38 | metaclass (~> 0.0.1) 39 | multi_json (1.12.1) 40 | multi_test (0.1.2) 41 | netrc (0.11.0) 42 | plist (3.1.0) 43 | rack (1.6.4) 44 | rack-protection (1.5.3) 45 | rack 46 | rake (11.1.2) 47 | rdoc (4.2.2) 48 | json (~> 1.4) 49 | rest-client (1.8.0) 50 | http-cookie (>= 1.0.2, < 2.0) 51 | mime-types (>= 1.16, < 3.0) 52 | netrc (~> 0.7) 53 | rspec (3.4.0) 54 | rspec-core (~> 3.4.0) 55 | rspec-expectations (~> 3.4.0) 56 | rspec-mocks (~> 3.4.0) 57 | rspec-core (3.4.4) 58 | rspec-support (~> 3.4.0) 59 | rspec-expectations (3.4.0) 60 | diff-lcs (>= 1.2.0, < 2.0) 61 | rspec-support (~> 3.4.0) 62 | rspec-mocks (3.4.1) 63 | diff-lcs (>= 1.2.0, < 2.0) 64 | rspec-support (~> 3.4.0) 65 | rspec-support (3.4.1) 66 | sinatra (1.4.7) 67 | rack (~> 1.5) 68 | rack-protection (~> 1.4) 69 | tilt (>= 1.3, < 3) 70 | thin (1.7.0) 71 | daemons (~> 1.0, >= 1.0.9) 72 | eventmachine (~> 1.0, >= 1.0.4) 73 | rack (>= 1, < 3) 74 | tilt (2.0.5) 75 | unf (0.1.4) 76 | unf_ext 77 | unf_ext (0.0.7.2) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | cucumber 84 | mimic! 85 | mocha 86 | rake 87 | rdoc 88 | rest-client 89 | rspec 90 | 91 | BUNDLED WITH 92 | 1.12.3 93 | -------------------------------------------------------------------------------- /mimic.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: mimic 0.4.3 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "mimic" 6 | s.version = "0.4.3" 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 9 | s.require_paths = ["lib"] 10 | s.authors = ["Luke Redpath"] 11 | s.date = "2016-06-07" 12 | s.email = "luke@lukeredpath.co.uk" 13 | s.extra_rdoc_files = ["README.md"] 14 | s.files = ["CHANGES", "LICENSE", "README.md", "Rakefile", "lib/mimic", "lib/mimic.rb", "lib/mimic/api.rb", "lib/mimic/fake_host.rb", "spec"] 15 | s.homepage = "http://lukeredpath.co.uk" 16 | s.rdoc_options = ["--main", "README.md"] 17 | s.rubygems_version = "2.4.5.1" 18 | s.summary = "A Ruby gem for faking external web services for testing" 19 | 20 | if s.respond_to? :specification_version then 21 | s.specification_version = 4 22 | 23 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 24 | s.add_runtime_dependency(%q, [">= 0"]) 25 | s.add_runtime_dependency(%q, [">= 0"]) 26 | s.add_runtime_dependency(%q, [">= 0"]) 27 | s.add_runtime_dependency(%q, [">= 0"]) 28 | s.add_runtime_dependency(%q, ["~> 3.1.0"]) 29 | s.add_development_dependency(%q, [">= 0"]) 30 | s.add_development_dependency(%q, [">= 0"]) 31 | s.add_development_dependency(%q, [">= 0"]) 32 | s.add_development_dependency(%q, [">= 0"]) 33 | else 34 | s.add_dependency(%q, [">= 0"]) 35 | s.add_dependency(%q, [">= 0"]) 36 | s.add_dependency(%q, [">= 0"]) 37 | s.add_dependency(%q, [">= 0"]) 38 | s.add_dependency(%q, ["~> 3.1.0"]) 39 | s.add_dependency(%q, [">= 0"]) 40 | s.add_dependency(%q, [">= 0"]) 41 | s.add_dependency(%q, [">= 0"]) 42 | s.add_dependency(%q, [">= 0"]) 43 | end 44 | else 45 | s.add_dependency(%q, [">= 0"]) 46 | s.add_dependency(%q, [">= 0"]) 47 | s.add_dependency(%q, [">= 0"]) 48 | s.add_dependency(%q, [">= 0"]) 49 | s.add_dependency(%q, ["~> 3.1.0"]) 50 | s.add_dependency(%q, [">= 0"]) 51 | s.add_dependency(%q, [">= 0"]) 52 | s.add_dependency(%q, [">= 0"]) 53 | s.add_dependency(%q, [">= 0"]) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/mimic.rb: -------------------------------------------------------------------------------- 1 | require 'mimic/fake_host' 2 | require 'singleton' 3 | require 'rack' 4 | require 'logger' 5 | require 'socket' 6 | 7 | module Mimic 8 | MIMIC_DEFAULT_PORT = 11988 9 | 10 | MIMIC_DEFAULT_OPTIONS = { 11 | :hostname => 'localhost', 12 | :port => MIMIC_DEFAULT_PORT, 13 | :remote_configuration_path => nil, 14 | :fork => true, 15 | :log => nil, 16 | :wait_timeout => 5 17 | } 18 | 19 | def self.mimic(options = {}, &block) 20 | options = MIMIC_DEFAULT_OPTIONS.merge(options) 21 | 22 | host = FakeHost.new(options).tap do |host| 23 | host.instance_eval(&block) if block_given? 24 | Server.instance.serve(host, options) 25 | end 26 | add_host(host) 27 | end 28 | 29 | def self.cleanup! 30 | Mimic::Server.instance.shutdown 31 | end 32 | 33 | def self.reset_all! 34 | @hosts.each { |h| h.clear } 35 | end 36 | 37 | private 38 | 39 | def self.add_host(host) 40 | host.tap { |h| (@hosts ||= []) << h } 41 | end 42 | 43 | class Server 44 | include Singleton 45 | 46 | def logger 47 | @logger ||= Logger.new(StringIO.new) 48 | end 49 | 50 | def serve(app, options) 51 | if options[:fork] 52 | @thread = Thread.fork do 53 | start_service(app, options) 54 | end 55 | 56 | wait_for_service(app.hostname, options[:port], options[:wait_timeout]) 57 | 58 | else 59 | start_service(app, options) 60 | end 61 | end 62 | 63 | def start_service(app, options) 64 | Rack::Handler::Thin.run(app.url_map, { 65 | :Port => options[:port], 66 | :Logger => logger, 67 | :AccessLog => logger, 68 | }) 69 | end 70 | 71 | def shutdown 72 | Thread.kill(@thread) if @thread 73 | end 74 | 75 | # courtesy of http://is.gd/eoYho 76 | 77 | def listening?(host, port) 78 | begin 79 | socket = TCPSocket.new(host, port) 80 | socket.close unless socket.nil? 81 | true 82 | rescue Errno::ECONNREFUSED, SocketError, 83 | Errno::EBADF, # Windows 84 | Errno::EADDRNOTAVAIL # Windows 85 | false 86 | end 87 | end 88 | 89 | def wait_for_service(host, port, timeout = 5) 90 | start_time = Time.now 91 | 92 | until listening?(host, port) 93 | if timeout && (Time.now > (start_time + timeout)) 94 | raise SocketError.new("Socket did not open within #{timeout} seconds") 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /features/stubbing_requests_by_path.feature: -------------------------------------------------------------------------------- 1 | Feature: Stubbing requests by path 2 | In order to test my app through its entire stack without depending on an external API 3 | As a developer 4 | I want to be able to stub requests to specific paths to return a canned response 5 | 6 | Scenario: Stubbing a GET request to /some/path and return an empty response 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988).get("/some/path") 10 | """ 11 | When I make an HTTP GET request to "http://localhost:11988/some/path" 12 | Then I should receive an HTTP 200 response with an empty body 13 | 14 | Scenario: Stubbing a GET request to /some/path and returning a non-empty response 15 | Given I have a mimic specification with: 16 | """ 17 | Mimic.mimic(:port => 11988).get("/some/path").returning("Hello World") 18 | """ 19 | When I make an HTTP GET request to "http://localhost:11988/some/path" 20 | Then I should receive an HTTP 200 response with a body matching "Hello World" 21 | 22 | Scenario: Requesting an un-stubbed path and getting a 404 response 23 | Given I have a mimic specification with: 24 | """ 25 | Mimic.mimic(:port => 11988).get("/some/path") 26 | """ 27 | When I make an HTTP GET request to "http://localhost:11988/some/other/path" 28 | Then I should receive an HTTP 404 response with an empty body 29 | 30 | Scenario: Stubbing a POST request to /some/path and return an empty response 31 | Given I have a mimic specification with: 32 | """ 33 | Mimic.mimic(:port => 11988).post("/some/path") 34 | """ 35 | When I make an HTTP POST request to "http://localhost:11988/some/path" 36 | Then I should receive an HTTP 200 response with an empty body 37 | 38 | Scenario: Stubbing a PUT request to /some/path and return an empty response 39 | Given I have a mimic specification with: 40 | """ 41 | Mimic.mimic(:port => 11988).put("/some/path") 42 | """ 43 | When I make an HTTP PUT request to "http://localhost:11988/some/path" 44 | Then I should receive an HTTP 200 response with an empty body 45 | 46 | Scenario: Stubbing a DELETE request to /some/path and return an empty response 47 | Given I have a mimic specification with: 48 | """ 49 | Mimic.mimic(:port => 11988).delete("/some/path") 50 | """ 51 | When I make an HTTP DELETE request to "http://localhost:11988/some/path" 52 | Then I should receive an HTTP 200 response with an empty body 53 | 54 | Scenario: Stubbing a HEAD request to /some/path and return an empty response 55 | Given I have a mimic specification with: 56 | """ 57 | Mimic.mimic(:port => 11988).head("/some/path") 58 | """ 59 | When I make an HTTP HEAD request to "http://localhost:11988/some/path" 60 | Then I should receive an HTTP 200 response with an empty body 61 | -------------------------------------------------------------------------------- /features/steps/http_client_steps.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | @httpclient = HttpClient.new 3 | end 4 | 5 | def headers_from_string(string) 6 | string.split("\n").inject({}) do |headers, header_string| 7 | headers.tap do |h| 8 | components = header_string.split(":") 9 | h[components[0].strip] = components[1].strip 10 | end 11 | end 12 | end 13 | 14 | When /^I make an HTTP (POST|PUT) request to "([^\"]*)" with the payload:$/ do |http_method, url, payload| 15 | @httpclient.perform_request_with_payload(url, http_method, payload) 16 | end 17 | 18 | When /^I make an HTTP (POST|PUT) request with a "([^\"]*)" content-type to "([^\"]*)" and the payload:$/ do |http_method, content_type, url, payload| 19 | @httpclient.perform_request_with_payload(url, http_method, payload, :content_type => content_type) 20 | end 21 | 22 | When /^I make an HTTP (GET|POST|PUT|DELETE|HEAD) request to "([^\"]*)"$/ do |http_method, url| 23 | @httpclient.perform_request(url, http_method) 24 | end 25 | 26 | When /^I make an HTTP (GET|POST|PUT|DELETE|HEAD) request to "([^\"]*)" with the header "([^\"]*)"$/ do |http_method, url, header| 27 | @httpclient.perform_request(url, http_method, nil, headers_from_string(header)) 28 | end 29 | 30 | Then /^I should receive an HTTP (\d+) response with an empty body$/ do |status_code| 31 | steps %Q{ 32 | Then I should receive an HTTP #{status_code} response with a body matching "" 33 | } 34 | end 35 | 36 | Then /^I should receive an HTTP (\d+) response with a body matching "([^\"]*)"$/ do |status_code, http_body| 37 | expect(@httpclient).to have_response_with_code_and_body(status_code.to_i, http_body) 38 | end 39 | 40 | Then /^I should receive an HTTP (\d+) response with a body containing:$/ do |status_code, http_body| 41 | expect(@httpclient).to have_response_with_code_and_body(status_code.to_i, http_body) 42 | end 43 | 44 | Then /^I should receive an HTTP (\d+) response$/ do |status_code| 45 | expect(@httpclient).to have_response_with_code(status_code.to_i) 46 | end 47 | 48 | Then /^I should receive an HTTP (\d+) response with the value "([^\"]*)" for the header "([^\"]*)"$/ do |status_code, header_value, header_key| 49 | expect(@httpclient).to have_response_with_code_and_header(status_code.to_i, header_key, header_value) 50 | end 51 | 52 | Then /^I should receive an HTTP (\d+) response with the JSON value "([^\"]*)" for the key path "([^\"]*)"$/ do |status, json_value, key_path| 53 | json = JSON.parse(@httpclient.last_response.to_s) 54 | json.value_for_key_path(key_path).should == json_value 55 | end 56 | 57 | Then /^I should receive an HTTP (\d+) response with the Plist value "([^\"]*)" for the key path "([^\"]*)"$/ do |status, json_value, key_path| 58 | plist = Plist.parse_xml(@httpclient.last_response.to_s) 59 | plist.value_for_key_path(key_path).should == json_value 60 | end 61 | 62 | # For debugging. You'll need to gem install bcat 63 | Then /^show me the response$/ do 64 | IO.popen("bcat", "w") do |bcat| 65 | bcat.puts @httpclient.last_response 66 | end 67 | end 68 | 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'cucumber' 3 | require 'cucumber/rake/task' 4 | require 'rspec/core/rake_task' 5 | 6 | ENV["MIMIC_TEST_PROXY"] = nil 7 | 8 | desc "Run all Cucumber features" 9 | Cucumber::Rake::Task.new(:features) do |t| 10 | t.cucumber_opts = "features --format pretty" 11 | end 12 | 13 | desc "Run all specs" 14 | RSpec::Core::RakeTask.new(:spec) do |t| 15 | end 16 | 17 | task :default => :spec 18 | task :all => [:spec, :features] 19 | 20 | require "rubygems/package_task" 21 | require "rdoc/task" 22 | 23 | # This builds the actual gem. For details of what all these options 24 | # mean, and other ones you can add, check the documentation here: 25 | # 26 | # http://rubygems.org/read/chapter/20 27 | # 28 | spec = Gem::Specification.new do |s| 29 | 30 | # Change these as appropriate 31 | s.name = "mimic" 32 | s.version = "0.4.4" 33 | s.summary = "A Ruby gem for faking external web services for testing" 34 | s.authors = "Luke Redpath" 35 | s.email = "luke@lukeredpath.co.uk" 36 | s.homepage = "http://lukeredpath.co.uk" 37 | 38 | s.has_rdoc = true 39 | s.extra_rdoc_files = %w(README.md) 40 | s.rdoc_options = %w(--main README.md) 41 | 42 | # Add any extra files to include in the gem 43 | s.files = %w(LICENSE CHANGES Rakefile README.md) + Dir.glob("{spec,lib/**/*}") 44 | s.require_paths = ["lib"] 45 | 46 | # If you want to depend on other gems, add them here, along with any 47 | # relevant versions 48 | s.add_dependency("rack") 49 | s.add_dependency("sinatra") 50 | s.add_dependency("thin") 51 | s.add_dependency("json") 52 | s.add_dependency("plist", "~> 3.1.0") 53 | 54 | # If your tests use any gems, include them here 55 | s.add_development_dependency("rspec") 56 | s.add_development_dependency("cucumber") 57 | s.add_development_dependency("mocha") 58 | s.add_development_dependency("rest-client") 59 | end 60 | 61 | # This task actually builds the gem. We also regenerate a static 62 | # .gemspec file, which is useful if something (i.e. GitHub) will 63 | # be automatically building a gem for this project. If you're not 64 | # using GitHub, edit as appropriate. 65 | # 66 | # To publish your gem online, install the 'gemcutter' gem; Read more 67 | # about that here: http://gemcutter.org/pages/gem_docs 68 | Gem::PackageTask.new(spec) do |pkg| 69 | pkg.gem_spec = spec 70 | end 71 | 72 | desc "Build the gemspec file #{spec.name}.gemspec" 73 | task :gemspec do 74 | file = File.dirname(__FILE__) + "/#{spec.name}.gemspec" 75 | File.open(file, "w") {|f| f << spec.to_ruby } 76 | end 77 | 78 | task :package => :gemspec 79 | 80 | # Generate documentation 81 | RDoc::Task.new do |rd| 82 | rd.main = "README.md" 83 | rd.rdoc_files.include("README.md", "lib/**/*.rb") 84 | rd.rdoc_dir = "rdoc" 85 | end 86 | 87 | desc 'Clear out RDoc and generated packages' 88 | task :clean => [:clobber_rdoc, :clobber_package] do 89 | rm "#{spec.name}.gemspec" 90 | end 91 | 92 | task 'Release if all specs pass' 93 | task :release => [:clean, :bundle, :spec, :features, :package] do 94 | system("gem push pkg/#{spec.name}-#{spec.version}.gem") 95 | end 96 | 97 | desc 'Install all gem dependencies' 98 | task :bundle => :gemspec do 99 | system("bundle") 100 | end 101 | -------------------------------------------------------------------------------- /lib/mimic/api.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'plist' 3 | 4 | module Mimic 5 | class API < Sinatra::Base 6 | class << self 7 | attr_accessor :host 8 | end 9 | 10 | def host 11 | self.class.host 12 | end 13 | 14 | %w{get post put delete head}.each do |verb| 15 | post "/#{verb}" do 16 | api_request = APIRequest.from_request(request, verb) 17 | api_request.setup_stubs_on(host) 18 | [201, {"Content-Type" => api_request.request_content_type}, api_request.response] 19 | end 20 | end 21 | 22 | post "/multi" do 23 | api_request = APIRequest.from_request(request) 24 | api_request.setup_stubs_on(host) 25 | [201, {"Content-Type" => api_request.request_content_type}, api_request.response] 26 | end 27 | 28 | post "/clear" do 29 | response_body = self.host.inspect 30 | self.host.clear 31 | [200, {}, "Cleared stubs: #{response_body}"] 32 | end 33 | 34 | get "/ping" do 35 | [200, {}, "OK"] 36 | end 37 | 38 | get "/debug" do 39 | [200, {}, self.host.inspect] 40 | end 41 | 42 | get "/requests" do 43 | [200, {"Content-Type" => "application/json"}, {"requests" => host.received_requests.map(&:to_hash)}.to_json] 44 | end 45 | 46 | private 47 | 48 | class APIRequest 49 | attr_reader :request_content_type 50 | 51 | def initialize(data, method = nil, request_content_type = '') 52 | @data = data 53 | @method = (method || "GET") 54 | @stubs = [] 55 | @request_content_type = request_content_type 56 | end 57 | 58 | def to_s 59 | @data.inspect 60 | end 61 | 62 | def response 63 | response = {"stubs" => @stubs.map(&:to_hash)} 64 | 65 | case request_content_type 66 | when /json/ 67 | response.to_json 68 | when /plist/ 69 | response.to_plist 70 | else 71 | response.to_json 72 | end 73 | end 74 | 75 | def self.from_request(request, method = nil) 76 | case request.content_type 77 | when /json/ 78 | data = JSON.parse(request.body.string) 79 | when /plist/ 80 | data = Plist.parse_xml(request.body.string) 81 | else 82 | data = JSON.parse(request.body.string) 83 | end 84 | new(data, method, request.content_type) 85 | end 86 | 87 | def setup_stubs_on(host) 88 | (@data["stubs"] || [@data]).each do |stub_data| 89 | @stubs << Stub.new(stub_data, stub_data['method'] || @method).on(host) 90 | end 91 | end 92 | 93 | class Stub 94 | def initialize(data, method = nil) 95 | @data = data 96 | @method = method 97 | end 98 | 99 | def on(host) 100 | host.send(@method.downcase.to_sym, path).returning(body, code, headers).tap do |stub| 101 | stub.with_query_parameters(params) 102 | stub.echo_request!(echo_format) 103 | end 104 | end 105 | 106 | def echo_format 107 | @data['echo'].to_sym rescue nil 108 | end 109 | 110 | def path 111 | @data['path'] || '/' 112 | end 113 | 114 | def body 115 | @data['body'] || '' 116 | end 117 | 118 | def code 119 | @data['code'] || 200 120 | end 121 | 122 | def headers 123 | @data['headers'] || {} 124 | end 125 | 126 | def params 127 | @data['params'] || {} 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /features/echoing_request_in_response.feature: -------------------------------------------------------------------------------- 1 | Feature: Echoing request in response 2 | In order to easily verify that I sent the correct request data 3 | As a developer 4 | I want to tell mimic to echo the request data in a specific format in it's response 5 | 6 | Scenario: Echoing query parameters in JSON format 7 | Given I have a mimic specification with: 8 | """ 9 | Mimic.mimic(:port => 11988).get("/some/path").echo_request!(:json) 10 | """ 11 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 12 | Then I should receive an HTTP 200 response with the JSON value "bar" for the key path "echo.params.foo" 13 | 14 | Scenario: Echoing request headers in JSON format 15 | Given I have a mimic specification with: 16 | """ 17 | Mimic.mimic(:port => 11988).get("/some/path").echo_request!(:json) 18 | """ 19 | When I make an HTTP GET request to "http://localhost:11988/some/path" with the header "X-TEST: Some Value" 20 | Then I should receive an HTTP 200 response with the JSON value "Some Value" for the key path "echo.env.HTTP_X_TEST" 21 | 22 | Scenario: Echoing request body in JSON format 23 | Given I have a mimic specification with: 24 | """ 25 | Mimic.mimic(:port => 11988).post("/some/path").echo_request!(:json) 26 | """ 27 | When I make an HTTP POST request to "http://localhost:11988/some/path" with the payload: 28 | """ 29 | REQUEST BODY 30 | """ 31 | Then I should receive an HTTP 200 response with the JSON value "REQUEST BODY" for the key path "echo.body" 32 | 33 | Scenario: Echoing query parameters in Plist format 34 | Given I have a mimic specification with: 35 | """ 36 | Mimic.mimic(:port => 11988).get("/some/path").echo_request!(:plist) 37 | """ 38 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 39 | Then I should receive an HTTP 200 response with the Plist value "bar" for the key path "echo.params.foo" 40 | 41 | Scenario: Echoing request headers in Plist format 42 | Given I have a mimic specification with: 43 | """ 44 | Mimic.mimic(:port => 11988).get("/some/path").echo_request!(:plist) 45 | """ 46 | When I make an HTTP GET request to "http://localhost:11988/some/path" with the header "X-TEST: Some Value" 47 | Then I should receive an HTTP 200 response with the Plist value "Some Value" for the key path "echo.env.HTTP_X_TEST" 48 | 49 | Scenario: Echoing request body in Plist format 50 | Given I have a mimic specification with: 51 | """ 52 | Mimic.mimic(:port => 11988).post("/some/path").echo_request!(:plist) 53 | """ 54 | When I make an HTTP POST request to "http://localhost:11988/some/path" with the payload: 55 | """ 56 | REQUEST BODY 57 | """ 58 | Then I should receive an HTTP 200 response with the Plist value "REQUEST BODY" for the key path "echo.body" 59 | 60 | Scenario: Echoing query parameters but also specifying a response body results in response body being overwritten 61 | Given I have a mimic specification with: 62 | """ 63 | Mimic.mimic(:port => 11988).get("/some/path").returning("not an echo").echo_request!(:json) 64 | """ 65 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 66 | Then I should receive an HTTP 200 response with the JSON value "bar" for the key path "echo.params.foo" 67 | 68 | Scenario: Echoing response manually from block definition 69 | Given I have a mimic specification with: 70 | """ 71 | Mimic.mimic(:port => 11988).get("/some/path") do 72 | echo_request!(:json) 73 | end 74 | """ 75 | When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar" 76 | Then I should receive an HTTP 200 response with the JSON value "bar" for the key path "echo.params.foo" 77 | -------------------------------------------------------------------------------- /spec/fake_host_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Mimic::FakeHost" do 4 | before do 5 | @host = Mimic::FakeHost.new(:hostname => "www.example.com") 6 | end 7 | 8 | it "should handle stubbed requests" do 9 | @host.get("/some/path") 10 | expect(@host.call(request_for("/some/path"))).to match_rack_response(200, {}, "") 11 | end 12 | 13 | it "should handle stubbed requests that return a response" do 14 | @host.get("/some/path").returning("hello world") 15 | expect(@host.call(request_for("/some/path"))).to match_rack_response(200, {}, "hello world") 16 | end 17 | 18 | it "should handle stubbed requests that return a specific HTTP code" do 19 | @host.get("/some/path").returning("redirecting", 301) 20 | expect(@host.call(request_for("/some/path"))).to match_rack_response(301, {}, "redirecting") 21 | end 22 | 23 | it "should handle stubbed requests that return specific headers" do 24 | @host.get("/some/path").returning("redirecting", 301, {"Location" => "http://somewhereelse.com"}) 25 | expect(@host.call(request_for("/some/path"))).to match_rack_response(301, {"Location" => "http://somewhereelse.com"}, "redirecting") 26 | end 27 | 28 | it "should not recognize requests if they have the incorrect HTTP method" do 29 | @host.get("/some/path") 30 | expect(@host.call(request_for("/some/path", :method => "POST"))).to match_rack_response(404, {}, "") 31 | end 32 | 33 | it "should not handle multiple requests to a path with different HTTP methods" do 34 | @host.get("/some/path").returning("GET Request", 200) 35 | @host.post("/some/path").returning("POST Request", 201) 36 | expect(@host.call(request_for("/some/path", :method => "GET"))).to match_rack_response(200, {}, "GET Request") 37 | expect(@host.call(request_for("/some/path", :method => "POST"))).to match_rack_response(201, {}, "POST Request") 38 | end 39 | 40 | it "should handle requests with behaviour specified in a block using the Sinatra API" do 41 | @host.get("/some/path") do 42 | content_type 'text/plain' 43 | 'bobby' 44 | end 45 | expect(@host.call(request_for("/some/path", :method => "GET"))).to match_rack_response(200, {'Content-Type' => 'text/plain;charset=utf-8'}, 'bobby') 46 | end 47 | 48 | it "should allow stubs to be cleared" do 49 | @host.get("/some/path") 50 | expect(@host.call(request_for("/some/path"))).to match_rack_response(200, {}, "") 51 | @host.clear 52 | expect(@host.call(request_for("/some/path"))).to match_rack_response(404, {}, "") 53 | end 54 | 55 | it "should allow stubs to be imported from a file" do 56 | @host.import(File.join(File.dirname(__FILE__), *%w[fixtures import_stubs.mimic])) 57 | expect(@host.call(request_for("/imported/path"))).to match_rack_response(200, {}, "") 58 | end 59 | 60 | it "should not clear imported stubs" do 61 | @host.import(File.join(File.dirname(__FILE__), *%w[fixtures import_stubs.mimic])) 62 | @host.clear 63 | expect(@host.call(request_for("/imported/path"))).to match_rack_response(200, {}, "") 64 | end 65 | 66 | it "should raise if import file does not exist" do 67 | expect { 68 | @host.import(File.join(File.dirname(__FILE__), *%w[fixtures doesnt_exist.mimic])) 69 | }.to raise_error(RuntimeError) 70 | end 71 | 72 | it "returns a StubbedRequest" do 73 | expect(@host.get("/some/path")).to be_kind_of(Mimic::FakeHost::StubbedRequest) 74 | end 75 | 76 | describe "StubbedRequest" do 77 | it "has a unique hash based on it's parameters" do 78 | host = Mimic::FakeHost::StubbedRequest.new(stub, "GET", "/path") 79 | expect(host.to_hash).to eq(Digest::MD5.hexdigest("GET /path")) 80 | end 81 | 82 | it "has the same hash as an equivalent request" do 83 | host_one = Mimic::FakeHost::StubbedRequest.new(stub, "GET", "/path") 84 | host_two = Mimic::FakeHost::StubbedRequest.new(stub, "GET", "/path") 85 | expect(host_one.to_hash).to eq(host_two.to_hash) 86 | end 87 | end 88 | 89 | private 90 | 91 | def request_for(path, options={}) 92 | options = {:method => "GET"}.merge(options) 93 | { "PATH_INFO" => path, 94 | "REQUEST_METHOD" => options[:method], 95 | "rack.errors" => StringIO.new, 96 | "rack.input" => StringIO.new } 97 | end 98 | 99 | RSpec::Matchers.define :match_rack_response do |code, headers, body| 100 | match do |actual| 101 | (actual[0] == code) && 102 | (headers.all? {|k, v| actual[1][k] == v }) && 103 | (actual[2].include?(body)) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mimic, simple web service stubs for testing [![Build Status](https://secure.travis-ci.org/lukeredpath/mimic.png)](https://secure.travis-ci.org/lukeredpath/mimic) 2 | 3 | ## What is Mimic? 4 | 5 | Mimic is a testing tool that lets you set create a fake stand-in for an external web service to be used when writing integration/end-to-end tests for applications or libraries that access these services. 6 | 7 | ## Why not stub? 8 | There are already some good tools, like [FakeWeb](http://fakeweb.rubyforge.org/) which let you stub requests at a low-level which is fine for unit and functional tests but when exercising our code through integration or end-to-end tests we want to exercise as much of the stack as possible. 9 | 10 | Mimic aims to make it possible to test your networking code without actually hitting the real services by starting up a real web server and responding to HTTP requests. This lets you test your application against canned responses in an as-close-to-the-real-thing-as-possible way. 11 | 12 | Also, because Mimic responds to real HTTP requests, it can be used when testing non-Ruby applications too. 13 | 14 | ## Examples 15 | 16 | Registering to a single request stub: 17 | 18 | Mimic.mimic.get("/some/path").returning("hello world") 19 | 20 | And the result, using RestClient: 21 | 22 | $ RestClient.get("http://www.example.com:11988/some/path") # => 200 | hello world 23 | 24 | Registering multiple request stubs; note that you can stub the same path with different HTTP methods separately. 25 | 26 | Mimic.mimic do 27 | get("/some/path").returning("Hello World", 200) 28 | get("/some/other/path").returning("Redirecting...", 301, {"Location" => "somewhere else"}) 29 | post("/some/path").returning("Created!", 201) 30 | end 31 | 32 | You can even use Rack middlewares, e.g. to handle common testing scenarios such as authentication: 33 | 34 | Mimic.mimic do 35 | use Rack::Auth::Basic do |user, pass| 36 | user == 'theuser' and pass == 'thepass' 37 | end 38 | 39 | get("/some/path") 40 | end 41 | 42 | Finally, because Mimic is built on top of Sinatra for the core request handling, you can create your stubbed requests like you would in any Sinatra app: 43 | 44 | Mimic.mimic do 45 | get "/some/path" do 46 | [200, {}, "hello world"] 47 | end 48 | end 49 | 50 | ## Using Mimic with non-Ruby processes 51 | 52 | Mimic has a built-in REST API that lets you configure your request stubs over HTTP. This makes it possible to use Mimic from other processes that can perform HTTP requests. 53 | 54 | First of all, you'll need to run Mimic as a daemon. You can do this with a simple Ruby script and the [daemons](http://daemons.rubyforge.org/) gem: 55 | 56 | #!/usr/bin/env ruby 57 | require 'mimic' 58 | require 'daemons' 59 | 60 | Daemons.run_proc("mimic") do 61 | Mimic.mimic(:port => 11988, :fork => false, :remote_configuration_path => '/api') do 62 | # configure your stubs here 63 | end 64 | end 65 | 66 | Give the script executable permissions and then start it: 67 | 68 | $ your_mimic_script.rb start (or run) 69 | 70 | The remote configuration path is where the API endpoints will be mounted - this is configurable as you will not be able this path or any paths below it in your stubs, so choose one that doesn't conflict with the paths you need to stub. 71 | 72 | The API supports both JSON and Plist payloads, defaulting to JSON. Set the request Content-Type header to application/plist for Plist requests. 73 | 74 | For the following Mimic configuration (using the Ruby DSL): 75 | 76 | Mimic.mimic.get("/some/path").returning("hello world") 77 | 78 | The equivalent stub can be configured using the REST API as follows: 79 | 80 | $ curl -d'{"path":"/some/path", "body":"hello world"}' http://localhost:11988/api/get 81 | 82 | Likewise, a POST request to the same path could be stubbed like so: 83 | 84 | $ curl -d'{"path":"/some/path", "body":"hello world"}' http://localhost:11988/api/post 85 | 86 | The end-point of the API is the HTTP verb you are stubbing, the path, response body, code and headers are specified in the POST data (a hash in JSON or Plist format). See the HTTP API Cucumber features for more examples. 87 | 88 | An [Objective-C wrapper](http://github.com/lukeredpath/LRMimic) for the REST API is available, allowing you to use mimic for your OSX and iOS apps. 89 | 90 | ## Contributors 91 | 92 | * [James Fairbairn](http://github.com/jfairbairn) 93 | * [Marcello Barnaba](https://github.com/vjt) 94 | 95 | ## License 96 | 97 | As usual, the code is released under the MIT license which is included in the repository. 98 | 99 | -------------------------------------------------------------------------------- /lib/mimic/fake_host.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'mimic/api' 3 | 4 | module Mimic 5 | class FakeHost 6 | attr_reader :hostname, :url_map 7 | attr_accessor :log 8 | 9 | def initialize(options = {}) 10 | @hostname = options[:hostname] 11 | @remote_configuration_path = options[:remote_configuration_path] 12 | @log = options[:log] 13 | @imports = [] 14 | clear 15 | build_url_map! 16 | end 17 | 18 | def received_requests 19 | @stubs.select { |s| s.received } 20 | end 21 | 22 | def get(path, &block) 23 | request("GET", path, &block) 24 | end 25 | 26 | def post(path, &block) 27 | request("POST", path, &block) 28 | end 29 | 30 | def put(path, &block) 31 | request("PUT", path, &block) 32 | end 33 | 34 | def delete(path, &block) 35 | request("DELETE", path, &block) 36 | end 37 | 38 | def head(path, &block) 39 | request("HEAD", path, &block) 40 | end 41 | 42 | def import(path) 43 | if File.exists?(path) 44 | @imports << path unless @imports.include?(path) 45 | instance_eval(File.read(path)) 46 | else 47 | raise "Could not locate file for stub import: #{path}" 48 | end 49 | end 50 | 51 | def call(env) 52 | @stubs.each(&:build) 53 | @app.call(env) 54 | end 55 | 56 | def clear 57 | @stubs = [] 58 | @app = Sinatra.new 59 | @app.use Rack::CommonLogger, self.log if self.log 60 | @app.not_found do 61 | [404, {}, ""] 62 | end 63 | @app.helpers do 64 | include Helpers 65 | end 66 | @imports.each { |file| import(file) } 67 | end 68 | 69 | def inspect 70 | @stubs.inspect 71 | end 72 | 73 | private 74 | 75 | def method_missing(method, *args, &block) 76 | @app.send(method, *args, &block) 77 | end 78 | 79 | def request(method, path, &block) 80 | if block_given? 81 | @app.send(method.downcase, path, &block) 82 | else 83 | @stubs << StubbedRequest.new(@app, method, path) 84 | @stubs.last 85 | end 86 | end 87 | 88 | def build_url_map! 89 | routes = {'/' => self} 90 | 91 | if @remote_configuration_path 92 | API.host = self 93 | routes[@remote_configuration_path] = API 94 | end 95 | 96 | @url_map = Rack::URLMap.new(routes) 97 | end 98 | 99 | module Helpers 100 | def echo_request!(format) 101 | RequestEcho.new(request).response_as(format) 102 | end 103 | end 104 | 105 | class RequestEcho 106 | def initialize(request) 107 | @request = request 108 | end 109 | 110 | def response_as(format) 111 | content_type = case format 112 | when :json, :plist 113 | "application/#{format.to_s.downcase}" 114 | else 115 | "text/plain" 116 | end 117 | [200, {"Content-Type" => content_type}, to_s(format)] 118 | end 119 | 120 | def to_s(format) 121 | case format 122 | when :json 123 | to_hash.to_json 124 | when :plist 125 | to_hash.to_plist 126 | when :text 127 | to_hash.inspect 128 | end 129 | end 130 | 131 | def to_hash 132 | {"echo" => { 133 | "params" => @request.params, 134 | "env" => env_without_rack_and_async_env, 135 | "body" => @request.body.read 136 | }} 137 | end 138 | 139 | private 140 | 141 | def env_without_rack_and_async_env 142 | Hash[*@request.env.select { |key, value| key !~ /^(rack|async)/i }.flatten] 143 | end 144 | end 145 | 146 | class StubbedRequest 147 | attr_accessor :received 148 | 149 | def initialize(app, method, path) 150 | @method, @path = method, path 151 | @code = 200 152 | @headers = {} 153 | @params = {} 154 | @body = "" 155 | @app = app 156 | @received = false 157 | end 158 | 159 | def to_hash 160 | token = "#{@method} #{@path}" 161 | Digest::MD5.hexdigest(token) 162 | end 163 | 164 | def returning(body, code = 200, headers = {}) 165 | tap do 166 | @body = body 167 | @code = code 168 | @headers = headers 169 | end 170 | end 171 | 172 | def with_query_parameters(params) 173 | tap do 174 | @params = params 175 | end 176 | end 177 | 178 | def echo_request!(format = :json) 179 | @echo_request_format = format 180 | end 181 | 182 | def matches?(request) 183 | if @params.any? 184 | request.params == @params 185 | else 186 | true 187 | end 188 | end 189 | 190 | def matched_response 191 | [@code, @headers, @body] 192 | end 193 | 194 | def unmatched_response 195 | [404, {}, ""] 196 | end 197 | 198 | def response_for_request(request) 199 | if @echo_request_format 200 | @body = RequestEcho.new(request).to_s(@echo_request_format) 201 | end 202 | 203 | matches?(request) ? matched_response : unmatched_response 204 | end 205 | 206 | def build 207 | stub = self 208 | 209 | @app.send(@method.downcase, @path) do 210 | stub.received = true 211 | stub.response_for_request(request) 212 | end 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /features/configuring_mimic_via_http.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring Mimic via an HTTP interface 2 | In order to use Mimic stubs from non-Ruby test cases 3 | As a developer 4 | I want to be able to configure a background Mimic process using an HTTP REST API 5 | 6 | Scenario: Pinging Mimic via the API to check it's running 7 | Given that Mimic is running and accepting remote configuration on "/api" 8 | When I make an HTTP GET request to "http://localhost:11988/api/ping" 9 | Then I should receive an HTTP 200 response with a body matching "OK" 10 | 11 | Scenario: Stubbing a request path via GET using the HTTP API 12 | Given that Mimic is running and accepting remote configuration on "/api" 13 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 14 | """ 15 | {"path": "/anything"} 16 | """ 17 | Then I should receive an HTTP 201 response 18 | And I make an HTTP GET request to "http://localhost:11988/anything" 19 | Then I should receive an HTTP 200 response with an empty body 20 | 21 | Scenario: Stubbing a request path via POST the HTTP API 22 | Given that Mimic is running and accepting remote configuration on "/api" 23 | When I make an HTTP POST request to "http://localhost:11988/api/post" with the payload: 24 | """ 25 | {"path": "/anything"} 26 | """ 27 | Then I should receive an HTTP 201 response 28 | And I make an HTTP POST request to "http://localhost:11988/anything" 29 | Then I should receive an HTTP 200 response with an empty body 30 | 31 | Scenario: Stubbing a request path via PUT using the HTTP API 32 | Given that Mimic is running and accepting remote configuration on "/api" 33 | When I make an HTTP POST request to "http://localhost:11988/api/put" with the payload: 34 | """ 35 | {"path": "/anything"} 36 | """ 37 | Then I should receive an HTTP 201 response 38 | And I make an HTTP PUT request to "http://localhost:11988/anything" 39 | Then I should receive an HTTP 200 response with an empty body 40 | 41 | Scenario: Stubbing a request path via DELETE the HTTP API for a 42 | Given that Mimic is running and accepting remote configuration on "/api" 43 | When I make an HTTP POST request to "http://localhost:11988/api/delete" with the payload: 44 | """ 45 | {"path": "/anything"} 46 | """ 47 | Then I should receive an HTTP 201 response 48 | And I make an HTTP DELETE request to "http://localhost:11988/anything" 49 | Then I should receive an HTTP 200 response with an empty body 50 | 51 | Scenario: Stubbing a request path via HEAD using the HTTP API 52 | Given that Mimic is running and accepting remote configuration on "/api" 53 | When I make an HTTP POST request to "http://localhost:11988/api/head" with the payload: 54 | """ 55 | {"path": "/anything"} 56 | """ 57 | Then I should receive an HTTP 201 response 58 | And I make an HTTP HEAD request to "http://localhost:11988/anything" 59 | Then I should receive an HTTP 200 response with an empty body 60 | 61 | Scenario: Stubbing a request path to return a custom response body 62 | Given that Mimic is running and accepting remote configuration on "/api" 63 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 64 | """ 65 | {"path": "/anything", "body": "Hello World"} 66 | """ 67 | Then I should receive an HTTP 201 response 68 | And I make an HTTP GET request to "http://localhost:11988/anything" 69 | Then I should receive an HTTP 200 response with a body matching "Hello World" 70 | 71 | Scenario: Stubbing a request path to return a custom status code 72 | Given that Mimic is running and accepting remote configuration on "/api" 73 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 74 | """ 75 | {"path": "/anything", "code": 301} 76 | """ 77 | Then I should receive an HTTP 201 response 78 | And I make an HTTP GET request to "http://localhost:11988/anything" 79 | Then I should receive an HTTP 301 response with an empty body 80 | 81 | Scenario: Stubbing a request path to return custom headers 82 | Given that Mimic is running and accepting remote configuration on "/api" 83 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 84 | """ 85 | {"path": "/anything", "headers": {"X-TEST-HEADER": "TESTING"}} 86 | """ 87 | Then I should receive an HTTP 201 response 88 | And I make an HTTP GET request to "http://localhost:11988/anything" 89 | Then I should receive an HTTP 200 response with the value "TESTING" for the header "X-TEST-HEADER" 90 | 91 | Scenario: Stubbing a request path that only matches with the right query params 92 | Given that Mimic is running and accepting remote configuration on "/api" 93 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 94 | """ 95 | {"path": "/anything", "params": {"foo": "bar"}} 96 | """ 97 | Then I should receive an HTTP 201 response 98 | And I make an HTTP GET request to "http://localhost:11988/anything" 99 | Then I should receive an HTTP 404 response 100 | And I make an HTTP GET request to "http://localhost:11988/anything?foo=bar" 101 | Then I should receive an HTTP 200 response 102 | 103 | Scenario: Stubbing a request to echo it's request 104 | Given that Mimic is running and accepting remote configuration on "/api" 105 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 106 | """ 107 | {"path": "/anything", "echo": "json"} 108 | """ 109 | Then I should receive an HTTP 201 response 110 | And I make an HTTP GET request to "http://localhost:11988/anything?foo=bar" 111 | Then I should receive an HTTP 200 response with the JSON value "bar" for the key path "echo.params.foo" 112 | 113 | Scenario: Stubbing a request using the HTTP API in plist format 114 | Given that Mimic is running and accepting remote configuration on "/api" 115 | When I make an HTTP POST request with a "application/plist" content-type to "http://localhost:11988/api/get" and the payload: 116 | """ 117 | 118 | 119 | 120 | 121 | path 122 | /anything 123 | 124 | 125 | """ 126 | Then I should receive an HTTP 201 response 127 | And I make an HTTP GET request to "http://localhost:11988/anything" 128 | Then I should receive an HTTP 200 response with an empty body 129 | 130 | Scenario: Configuring multiple stubs for a single verb in a single request 131 | Given that Mimic is running and accepting remote configuration on "/api" 132 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 133 | """ 134 | {"stubs":[{"path": "/anything"}, {"path": "/something"}]} 135 | """ 136 | Then I should receive an HTTP 201 response 137 | And I make an HTTP GET request to "http://localhost:11988/anything" 138 | Then I should receive an HTTP 200 response with an empty body 139 | And I make an HTTP GET request to "http://localhost:11988/something" 140 | Then I should receive an HTTP 200 response with an empty body 141 | 142 | Scenario: Configuring multiple stubs for different verbs in a single request 143 | Given that Mimic is running and accepting remote configuration on "/api" 144 | When I make an HTTP POST request to "http://localhost:11988/api/multi" with the payload: 145 | """ 146 | {"stubs":[{"method": "GET", "path": "/anything"}, {"method": "POST", "path": "/something"}]} 147 | """ 148 | Then I should receive an HTTP 201 response 149 | And I make an HTTP GET request to "http://localhost:11988/anything" 150 | Then I should receive an HTTP 200 response with an empty body 151 | And I make an HTTP POST request to "http://localhost:11988/something" 152 | Then I should receive an HTTP 200 response with an empty body 153 | 154 | Scenario: Clearing all stubs via the HTTP API 155 | Given that Mimic is running and accepting remote configuration on "/api" with the existing stubs: 156 | """ 157 | get("/anything").returning("hello world") 158 | """ 159 | When I make an HTTP POST request to "http://localhost:11988/api/clear" 160 | And I make an HTTP GET request to "http://localhost:11988/anything" 161 | Then I should receive an HTTP 404 response with an empty body 162 | 163 | Scenario: Clearing all stubs then resetting a stub via the API 164 | Given that Mimic is running and accepting remote configuration on "/api" with the existing stubs: 165 | """ 166 | get("/anything").returning("hello world") 167 | """ 168 | When I make an HTTP POST request to "http://localhost:11988/api/clear" 169 | When I make an HTTP POST request to "http://localhost:11988/api/get" with the payload: 170 | """ 171 | {"path": "/anything", "body": "something else"} 172 | """ 173 | And I make an HTTP GET request to "http://localhost:11988/anything" 174 | Then I should receive an HTTP 200 response with a body matching "something else" 175 | --------------------------------------------------------------------------------