ããã«ã¡ã¯ãä¼å¡äºæ¥é¨ã®å°å®¤ (id:hogelog) ã§ããæ°ã¥ãã°å¼ç¤¾ã«å ¥ç¤¾ãã¦ããï¼å¹´ã¨2ã¶æãçµã£ã¦ãã¾ããã
ä»åã¯ãã®2å¹´2ã¶æã§åãã¦ä¼ç¤¾ãããã¯ãã rails new
ããRailsã¢ããªã±ã¼ã·ã§ã³ã¨ããã®ã¢ããªã±ã¼ã·ã§ã³ã§å©ç¨ããRack::VCR
(https://github.com/miyagawa/rack-vcr) ã«ã¤ãã¦ç°¡åã«è§£èª¬ãã¾ãã
æ°è¦ã¢ããªã±ã¼ã·ã§ã³ã®æ§æ
ä»åç§ãæ°è¦ã«ä½æããRailsã¢ããªã±ã¼ã·ã§ã³ã¯ä»®ã«ããã§ã¯omoikaneï¼ä»®ï¼ã¨å¼ã¶ãã¨ã«ãã¾ããomoikaneã¯ãªã¯ã¨ã¹ããããã¨ç¤¾å ã®æ±ç¨APIãµã¼ãã«ã¢ã¯ã»ã¹ããAPIãµã¼ãããåå¾ããæ å ±ãå ã«ã¬ã¹ãã³ã¹ãè¿ãã¢ããªã±ã¼ã·ã§ã³ã§ããomoikaneã®å®è£ ã»æ§æãã®ãã®ã¯ãã»ã©é£ãããªãã£ãã®ã§ãããä¸ã¤åé¡ç¹ãããã¾ããã
ãã®ã¢ããªã±ã¼ã·ã§ã³ã¯APIã®ã¬ã¹ãã³ã¹ã®ä»æ§ãç ´å£çã«å¤æ´ãããå ´åã«æ£ããåä½ããªããªã£ã¦ãã¾ãã¾ãããã®ãããªå¤æ´ãå ¥ãã¦ãã¾ããªãããã«ã¯ããã¤ãã®é¸æè¢ãèãããã¾ãã
- æ°ãã¤ãã
- é¢ä¿ããã¢ããªã®ã³ã¼ãããã調ã¹ã¦åé¡ãªããã¨ã確èªãã
- é¢ä¿åä½*1ã«ç¢ºèªãã
- ãã¹ãã³ã¼ããæ¸ãã¦CIã§APIã®ç ´å£ãç¡ãããã§ãã¯ãç¶ãã
æ°ãã¤ããã®ã¯å¤§å¤ãªã®ã§ããã¡ãããã¹ãã³ã¼ãã§ãã§ãã¯ãã¦ãããããã®ã§ãããããããã«ã¯
- omoikaneå´ã§ã¯APIã®ã¬ã¹ãã³ã¹ãã¢ãã¯ããAPIã¢ãã¯ã¬ã¹ãã³ã¹ãå ã«ããã¬ã¹ãã³ã¹ãæ£ããããã¹ãã
- APIå´ã§ã¯omoikaneå´ã®ãªã¯ã¨ã¹ãã模ãããªã¯ã¨ã¹ãã«å¯¾ãã¦ãomoikaneå´ãå¿ è¦ã¨ããã¬ã¹ãã³ã¹ã¨ãªã£ã¦ããããã¹ããã
å¿ è¦ãããã¾ãã
æ±ç¨APIã¨omoikaneãå¥ã®Railsã¢ããªã§ã¯ãªããã¢ããªã·ãã¯Railsã¢ããªã®å¥ã¨ã³ããã¤ã³ããªã©ã§ãã£ããªããã£ã¨ç°¡åãªãã¹ãã§æ¸ãã ã§ãããããã¤ã¯ããµã¼ãã¹ã®ããã«ã¯ãããããªããããã°ã£ã¦æ¸ããã¨è¨ããããæ¸ããããããã¾ããããããã°ãã®ã¯ç²ãã¾ããç²ãããããã¾ããã
ããã§ç¾ããã®ãã¡ããã©ç¤¾å 㧠@KazuCocoa @adorechic @miyagawa ã®è°è«ããçã¾ããRack::VCRã§ãã
Rack::VCR
Rack::VCRã¨ã¯RailsãSinatraãªã©ã®Rackã¢ããªã±ã¼ã·ã§ã³ã«å°å ¥ãããã¨ã§ãã¢ããªã±ã¼ã·ã§ã³ ã¸ã® ãªã¯ã¨ã¹ãã¨ãã®ã¬ã¹ãã³ã¹ãVCRã«ã»ããå½¢å¼ã§åºåã»ã¾ãã¯VCRã«ã»ããã®ãã¼ã¿ãå ã«ã¢ãã¯ãµã¼ãã¨ãã¦åä½ããããã¨ãã§ããRackããã«ã¦ã§ã¢ã§ãã
ããAPIã¸ã®ãªã¯ã¨ã¹ãã¨ã¬ã¹ãã³ã¹ãä¸åº¦å®è¡ãã¦è¨é²ãããã¨ã§ãã¹ããã¼ã¿ãä½æããã®ãé常ã®VCRã§ããã®ã«å¯¾ããAPIãæä¾ããå´ãããããããã¹ããã¼ã¿ãçæããã¨ããèãæ¹ã§ãã
以ä¸ã«ä»åã®ã³ã¼ãä¾ã¨ã¨ãã«ãã®å½¹å²ã解説ãã¾ããããã§ä¾ç¤ºããã³ã¼ã㯠https://github.com/hogelog/rack-vcr-sample ã«ã¾ã¨ãã¦ããã¾ãã®ã§ã詳ããç¥ãããå ´åã¯ãã¡ããã確èªãã ããã
ãªã¯ã¨ã¹ãã®è¨é²
Rack::VCRã®åºæ¬æ©è½ã¯ãªã¯ã¨ã¹ãã®VCRã«ã»ããã¸ã®è¨é²ã§ãã
ä¾ã§ã¯ api/ ã¨ããAPIã¢ããªã®specã§Rack::VCRãå©ç¨ãã¦VCRã«ã»ãããè¨é²ãã¾ãã
if Rails.env.test? Rails.configuration.middleware.insert(0, Rack::VCR) end
api/config/initializers/rack_vcr.rb
ãã¹ãå®è¡æã®ã¿Rackããã«ã¦ã§ã¢ã®å é ã«Rack::VCRãå ¥ãã¦ããã¾ãã
RSpec.configure do |config| config.around(:each, type: :request) do |example| host! "api.example.com" vcr_cassette = example.metadata[:vcr] if vcr_cassette VCR.use_cassette(vcr_cassette, record: :all) do example.run end else example.run end end ... end
vcr: "cassette_name"
ã®ãããªã¡ã¿ãã¼ã¿ãã¤ããspecã§ã®ã¿VCRã«ã»ãããè¨é²ããããã«è¨å®ãã¦ããã¨ã以ä¸ã®ããã«èªç¶ãªå½¢ã§VCRã«ã»ãããçæããspecãæ¸ããã¨ãã§ãã¾ãã
RSpec.describe "Books", type: :request do ... describe "books#index", vcr: "books_index" do it "returns books" do get "/books" expect(response).to have_http_status(200) data = JSON.parse(response.body) expect(data.size).to eq(2) expect(data.map{|book| book["title"] }).to eq(%w(K&R Camel)) end end ...
api/spec/requests/books_spec.rb
https://github.com/hogelog/rack-vcr-sample ã¯ä¾ç¤ºã®ããã« api/spec/fixtures/cassettes 以ä¸ã®yamlãã¡ã¤ã«ï¼VCRã«ã»ããï¼ããªãã¸ããªã«è¿½å ãã¦ãã¾ãããå®éã«éç¨ããå ´åã¯specå®è¡ã®ãã³ã«å¤æ´ãçºçãã¦ãã¾ãã®ã§ .gitignore ã«å ¥ãããªã©ãªãã¸ããªã«å ¥ããªãéç¨ãé©åã§ãã
ãªã¯ã¨ã¹ãã®ã¢ãã¯
ããã¯Rack::VCRã®æ©è½ã§ã¯ãªãVCRã®æ©è½ãªã®ã§ãããRack::VCRã§è¨é²ããVCRã«ã»ããã¯ãã¹ãã«å©ç¨ãããã¨ãã§ãã¾ãã
ä¾ã§ã¯rails-app/ã¨ããapiãå©ç¨ããRailsã¢ããªã®ãã¹ãã§ä¸è¿°ã®Rack::VCRã§çæããVCRã«ã»ãããå©ç¨ãã¦ãã¾ãã
ããã¯ç¹ã«Rack::VCRç¹æã®å¦çã¯ãªãã§ããããã¡ããvcr: "cassete_name"
ã®ãããªæå®ãããspecã§ã®ã¿VCRã«ã»ãããå©ç¨ããããã«è¨å®ãã¾ãã
ï¼ã¾ãããã®ä¾ã ã¨æ´»ç¨ãã¦ã¾ãããmatch_requests_on
ã«æ¸¡ãå¤ã調æ´ãããã¨ã§æå³çã«ä¸é¨ã®ã¯ã¨ãªããã¹ãç¡è¦ãããã¨ã§id
ã®å¤ãä¸å®ã«ãªããããªãã¹ããè¨è¿°ãããã¨ãåºãã¾ãï¼
require "vcr" VCR.configure do |config| config.cassette_library_dir = "spec/fixtures/cassettes" config.hook_into :webmock end RSpec.configure do |config| config.around(:each) do |example| vcr_cassette = example.metadata[:vcr] if vcr_cassette match = example.metadata[:match] ? example.metadata[:match] : %i(host path query) VCR.use_cassette(vcr_cassette, record: :none, match_requests_on: match) do example.run end else example.run end end ... end
RSpec.describe BooksController, type: :controller do describe "#index", vcr: "books_index" do it "show books" do get :index expect(response).to have_http_status(200) expect(assigns(:books).map{|book| book["title"] }).to eq(%w(K&R Camel)) end end ... end
rails-app/spec/controllers/books_controller_spec.rb
ãªã¯ã¨ã¹ãã®åç
Rack::VCRã¯ä»¥ä¸ã®ããã«ç°¡åãªã³ã¼ãã§VCRã«ã»ãããå©ç¨ãã¦ã¬ã¹ãã³ã¹ããã¢ãã¯ãµã¼ãã¨ãã¦åä½ããããã¨ãã§ãã¾ãããã®ãããªã¢ãã¯ãµã¼ãã¼ã使ããã¨ã§ãVCRã«ã»ãããç´æ¥æ±ããªãRubyã¢ããªã±ã¼ã·ã§ã³ä»¥å¤ã®ã¢ããªã±ã¼ã·ã§ã³ã§ãVCRã«ã»ãããå©ç¨ã§ãã¾ãã
require "rack" require "rack/vcr" VCR.configure do |config| config.cassette_library_dir = File.join(File.dirname(__FILE__), "cassettes") end class MockApp def self.call(env) [501, {}, ["Not Implemented"]] end end app = Rack::Builder.new do use Rack::VCR, replay: true, cassette: "test" run MockApp end run app
ãã ãããã§ã¯ã©ã®ãªã¯ã¨ã¹ãã§ãä¸ã¤ã®ã«ã»ããã®ã¬ã¹ãã³ã¹ã®ã¿è¿ããããã¾ãæè»ãªå©ç¨ãã§ãã¾ããã
ãã£ã¦ä»¥ä¸ã®ããã«"HTTP_X_VCR_CASSETTE"
ããããä»ä¸ããããªã¯ã¨ã¹ãã®ã¿ãããã§ä¸ããããã«ã»ããã使ãããã«ãã¾ãã
class CassetteLocator def initialize(app) @app = app end def call(env) cassette = env["HTTP_X_VCR_CASSETTE"] match = (env["HTTP_X_VCR_MATCH"] || "path query").split.map(&:to_sym) if cassette VCR.use_cassette(cassette, record: :none, match_requests_on: match) do @app.call(env) end else @app.call end end end ... app = Rack::Builder.new do use CassetteLocator use Rack::VCR, replay: true run MockApp end run app
ãããé常ã®Rackã¢ããªã®ããã«èµ·åããã ãã§"HTTP_X_VCR_CASSETTE"
ããããä»ä¸ããããªã¯ã¨ã¹ãã®ã¿ãããã§ä¸ããããã«ã»ãããå©ç¨ããã¢ãã¯ã¬ã¹ãã³ã¹ãè¿ãããã«ãªãã¾ãã
$ bundle exec rackup [2015-10-09 02:07:08] INFO WEBrick 1.3.1 [2015-10-09 02:07:08] INFO ruby 2.2.0 (2014-12-25) [x86_64-darwin14] [2015-10-09 02:07:08] INFO WEBrick::HTTPServer#start: pid=76284 port=9292
$ curl -H 'X_VCR_CASSETTE: books_index' 'http://localhost:9292/books' | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 214 100 214 0 0 19113 0 --:--:-- --:--:-- --:--:-- 19454 [ { "id": 1, "title": "K&R", "created_at": "2015-10-08T11:45:00.000Z", "updated_at": "2015-10-08T11:45:00.000Z" }, { "id": 2, "title": "Camel", "created_at": "2015-10-08T11:45:00.000Z", "updated_at": "2015-10-08T11:45:00.000Z" } ]
ãã¾ã: Androidã¢ããªã®ãã¹ã
ä¸è¿°ã®ã¢ãã¯ãµã¼ããå©ç¨ããã¨Androidã¢ããªãªã©VCRã«ã»ãããç´æ¥èªããªãã¢ããªã±ã¼ã·ã§ã³ã®ãã¹ããå¯è½ã«ãªãã¾ãã
ã¢ãã¯ãµã¼ããå©ç¨ãã¦ä»¥ä¸ã®ãããªã¤ã³ã¿ã¼ãã§ã¼ã¹ã®APIã¯ã©ã¤ã¢ã³ãã¯ã©ã¹ã®ãã¹ããã¾ãã
public class ApiClient { public ApiClient(String url); public String getUrl(); public Observable<List<Book>> getBooks(); protected Request createRequest(String path); }
ãã¹ãæã«ã¯HTTP_X_VCR_CASSETTE
ãããã追å ã§ä»ä¸ããããã¢ãã¯ç¨APIã¯ã©ã¤ã³ããå©ç¨ãããã¨ã«ãã¾ãã
public class MockApiClient extends ApiClient { private final String cassette; public MockApiClient(String url, String cassette) { super(url); this.cassette = cassette; } @Override protected Request createRequest(String path) { return new Request.Builder() .url(getUrl() + path) .get() .addHeader("X_VCR_CASSETTE", cassette) .build(); } }
android-app/app/src/test/java/org/hogel/androidapp/MockApiClient.java
ç´°ãããã¨ã¯ https://github.com/hogelog/rack-vcr-sample/blob/master/android-app/app/src/test/java/org/hogel/androidapp/ApiClientTest.java çãç´æ¥èªãã§ãããã¨ãã¦ãã¡ãã£ã¨å·¥å¤«ããã¨ä»¥ä¸ã®æ§ã«ã¢ããã¼ã·ã§ã³ã§ã©ã®VCRã«ã»ãããå©ç¨ããããã¹ãæ¯ã«æå®ãããã¨ãåºæ¥ã¾ãã
@Test @VcrCassette("books_index") public void testBookIndex() { apiClient.getBooks().subscribe(new Action1<List<Book>>() { @Override public void call(List<Book> books) { assertThat(books.size(), is(2)); assertThat(books.get(0).getTitle(), is("K&R")); assertThat(books.get(1).getTitle(), is("Camel")); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { assertTrue(false); } }); }
android-app/app/src/test/java/org/hogel/androidapp/ApiClientTest.java
å¼ç¤¾ã§ã®å©ç¨ä¾
å ã«æ¸ãã社å æ±ç¨APIã¨omoikaneã«ããã¦ã¯ãããã®æ©è½ã®ãã¡è¨é²ã¨ã¢ãã¯ã®ã¿å©ç¨ãã¦ãã¾ãã
Rack::VCRãå©ç¨ãããã¹ã追å ã®æµãã¨ãã¦ã¯
- omoikaneã«specã追å ãã¦å®è¡
- å½ç¶å¯¾å¿ããã«ã»ãããåå¨ããªãã®ã§VCRã«æããã¾ã
$ ./bin/rspec FF Failures: 1) BooksController#index show books Failure/Error: get :index VCR::Errors::UnhandledHTTPRequestError: ================================================================================ An HTTP request has been made that VCR does not know how to handle: GET http://api.example.com/books ...
- ä¸è¨ã¨ã©ã¼ãå ã«æ±ç¨APIã«omoikaneãå¿ è¦ã¨ãããªã¯ã¨ã¹ããæããspecã追å ãå®è¡
- æ±ç¨APIã®specãçæããVCRã«ã»ããã使ã£ã¦omoikaneã®specãæåãããã¨ã確èªãã³ããã
ã®ãããªæµãã§ãããªã£ã¦ãã¾ãã
ã¾ãCIã§ã¯
- æ±ç¨APIã®CIãèµ°ãã¨omoikaneç¨ã®VCRã«ã»ãããçæãããS3ã«ã¢ãããã¼ãããã
- æ±ç¨APIã®CIãå®äºããæã«omoikaneã®CIãããã¯ããã
- omoikaneã¯ãã¹ãéå§æã«S3ããææ°ã®VCRã«ã»ããããã¦ã³ãã¼ããã¦ãã
ã®ããã«å¸¸ã«ææ°ã®æ±ç¨APIã¨omoikaneã®çµã¿åãããæ£ããåä½ãããã¨ããã¹ããç¶ããæ±ç¨APIã«ç ´å£çãªå¤æ´ãã³ããããã¦ãã¾ã£ãå ´åã«ããã«CIã§ãããæ¤ç¥ã§ããããã«ãã¦ãã¾ãã
ãã¹ã追å ã®æµãã«ãã
- ä¸è¨ã¨ã©ã¼ãå ã«æ±ç¨APIã«omoikaneãå¿ è¦ã¨ãããªã¯ã¨ã¹ããæããspecã追å ãå®è¡
ã¨ããé¨åã¯å·¥å¤«ããã°ãã£ã¨èªååã§ããããªæ°ãããã®ã§ãããç¾ç¶ã¡ãã£ã¨ããã°ã£ã¦ç®ã§èªãã§æã§æ¸ãä½æ¥ããã¦ãã¾ããæ¹åã®ä½å°ãããã¾ãã
æªæ¥
Rack::VCRèªä½ã¯ããªãæ±ç¨çãªæ©è½ãæä¾ããã©ã¤ãã©ãªã§ãããããã«ç¤ºããå©ç¨æ¹æ³ããã¹ããã©ã¯ãã£ã¹ã¨ã¯éãã¾ããã
ã¢ããªã±ã¼ã·ã§ã³é£æºãã¹ãã«é¢ãã¦ã¯omoikaneã¸ã®Rack::VCRå°å ¥ã«ã大ããè²¢ç®ãã¦ããã @taiki45 ã«ç«ãã¤ãã¦ãæ¥ã @KazuCocoa ãªã©ã¨æ¿è«ã交ãããRack::VCRã®æ¹åãªããã以å¤ã®ä½ããªããç¾ç¶ã®Rack::VCRã§ã¯è¶³ããªãã©ãããç®æãã¦é å¼µã£ã¦ããã®ã§ãã®ãã¡ã¾ãé¢ç½ããã®ãçã¾ãã¦ããã¨æãã®ã§æ¥½ãã¿ã«ãå¾ ã¡ä¸ããã
å¼ç¤¾ã®ãã¹ãã¨ã³ã¸ãã¢ã¯Rack::VCRã®ãããªéçºãã¼ã«ã®éçºã»éç¨ãããã¹ãã»éçºããã»ã¹ãã®ãã®ã®æ¹åãªã©ãå¼ç¤¾ã®ã¨ã³ã¸ãã¢ãªã³ã°ã®ä¸æ ¸é¨åãæ ãéè¦ãªå½¹è·ã§ãã
ãããé¢ç½ãããªã®ã§ç§èªèº«ã社å é 置転æãé¡ã£ã¦åå ¥ããããã¨æãããããã®å½¹è·ãªã®ã§ãèå³ã®ããæ¹ã¯ãã²ä¸åº¦éã³ã«æ¥ã¦ã¿ã¦ãã ããã*2
ã¯ãã¯ããã ãã¹ãã¨ã³ã¸ãã¢ã®åé
ãã以å¤ã®è·ç¨®ã®æ¹ããã¡ããããããåéãã¦ã¾ã
*1:å¤ãã®å ´å誰ãé¢ä¿åä½ãªã®ããèªæã§ã¯ãªã
*2:ã§ããã£ã±ããããªé«åº¦ãªãã¹ãã¨ã³ã¸ãã¢ãé«ãæè¡åãæã¤æè¡é¨ãã¤ã³ãã©é¨ãªã©ã®åãã¤ã¾ã¿é£ãããªããã¢ããªéçºãã¦ä¾¡å¤åµé ãã¦ããç¾å¨ã®é¨ç½²ãé¢ç½ãã®ã§ãããã°ãããã£ã¦ãããã¨æãã¾ã