VCR — czyli wygodne mockowanie requestów HTTP w testach

Mariusz Błaszczak
akra-polska
Published in
5 min readAug 23, 2018

Jeśli w swojej aplikacji posiadasz logikę, która odnosi się do API zewnętrznych serwisów przez HTTP, to z pewnością również zauważyłeś, że pisanie testów do takiego kodu nie jest łatwe ani przyjemne.
No ale jednak moje doświadczenie, a nawet badania naukowe(1, 2) pokazują, że pisanie testów jest dobre i potrzebne! Więc wyjścia nie ma i pisać te testy jakoś trzeba. Ale jak się do tego zabrać?

źródło: https://chriskottom.com/blog

Do wyboru mamy kilka opcji:

1. Pozwól testom odwoływać się do zewnętrznych serwisów, tak samo jak robi to kod produkcyjny.

To rozwiązanie, niestety, niesie za sobą wiele problemów. Społeczność developerów zdecydowanie odradza tę praktykę.
Ale dlaczego? Przecież testy, które komunikują się z tymi serwisami mają wiele zalet? Są bardziej wiarygodne, bo przecież moja logika korzysta z tych serwisów..
Podstawowym problemem jest to, że nasze testy będą ściśle związane z Internetem oraz z tym zewnętrznym serwisem. Czyli, jeśli na przykład na serwerze CI chwilowo zabraknie Internetu, to nasz build, i co za tym idzie również deployment przestanie działać. Ten sam problem wystąpi również, gdy zewnętrzny serwis będzie przypadkowo offline. Również wtedy nasz build będzie zablokowany i nie będziemy mogli robić deploymentu nowych featerów czy hotfixów na produkcje. Czy można wyobrazić sobie coś gorszego niż pożar na produkcji, szybki hotfix i potem zmaganie się z blokującym deployment buildem z powodu jakiegoś tam serwisu, który akurat przeprowadza maintenance?

Inną, ale równie ważną kwestią jest szybkość testów. Testy, które łączą się z zewnętrznymi serwisami przez Internet, będą, z oczywistych względów, duuuuużo wolniejsze niż takie, które się z Internetem nie łączą. O minusach wolnych testów raczej pisać nie trzeba. Każdy, kto miał jakąkolwiek styczność z TDD wie jaki to ból czekać na feedback za każdym razem kilkanaście sekund…

Okej, już wiemy, że bezpośrednie odwołania do serwisów przez Internet są złym rozwiązaniem. Jakie są inne opcje?

2. Mockowanie.

Każdy, kto pisał testy, z mockami również prawdopodobnie się spotkał. Mają one swoich zwolenników i przeciwników. Podstawowym problemem zniechęcającym do mocków jest to, że pisanie ich jest żmudne. Zwłaszcza jeśli chodzi o mockowanie requestów. Tutaj będziesz musiał pamiętać o wiele większej ilości rzeczy, niż w przypadku mockowania zwykłych klas czy metod. Requesty mają masę różnych parametrów, autentykacje, różne rodzaje responsów itd.

Na szczęście jest trzecie rozwiązanie:

3. Rewelacyjna biblioteka do Ruby o nazwie VCR.

Nazwa tej biblioteki wzięła się od Video Casette Recorder

W wielkim skrócie: bibiotekę możemy podpiąć bezpośrednio w teście, który łączy się z zewnętrznym serwisem. Podczas pierwszego uruchomienia testu, nasza aplikacja normalnie odwoła się do tego serwisu przez HTTP. Jednak podczas tego pierwszego uruchomienia, biblioteka nagra kasetę, a dokładniej mówiąc zapisze wszystkie szczegóły o requeście w jednym pliku yaml. Potem za każdym kolejnym razem, kiedy będziemy uruchamiali ten test, ani zewnętrzny serwis, ani nawet Internet już nie będą potrzebne! Zamiast odnosić się do serwisu przez HTTP, VCR samo wykona za nas tą żmudną pracę i zamockuje request korzystając z wartości z wcześniej nagranej kasety (wyżej wspomnianego pliku yaml).

I tym sposobem mamy:

  • wszystkie korzyści z mockowania requestów,
  • unikamy wszystkich problemów, które mogą towarzyszyć requestom do zewnętrznych serwisów bezpośrednio przez Internet.
  • A także unikamy tej żmudnej pracy mockowania requestów! :)

Uff trochę przydługi ten wstęp. Teraz więc szybko do dzieła. Na prostym przykładzie pokażę, jak można skorzystać z tego dobrodziejstrwa.

Do dzieła!

Na potrzeby tego artykułu stworzyłem prościutki projekcik w Railsach (który można podejrzeć tutaj)

Mamy w nim bardzo prostą klasę, w której korzystam z zewnętrznego serwisu przez API HTTP (W prawdziwej, produkcyjnej aplikacji, rzadko zdarzają się sytuacje, że tylko jeden test i tylko jedno miejsce w kodzie odwołują się do zewnętrznych serwisów. Dlatego specjalnie uruchamiam ten request do API 100 razy, aby zasymulować realne warunki i tym samym pokazać jak wolne mogą być takie testy):

# services/posts.rbclass Posts
include HTTParty
base_uri ‘jsonplaceholder.typicode.com’
def fetch
records = []
100.times do
records = records + self.class.get(‘/posts’)
end
records
end
end

oraz test:

# spec/services/posts_spec.rbrequire ‘rails_helper’RSpec.describe Posts do
it ‘fetches all posts from external api service’ do
expect(Posts.new.fetch.count).to eq(100*100)
end
end

Uruchomiłem test:

❯ rspec
.
Finished in 3.58 seconds (files took 0.27433 seconds to load)
1 example, 0 failures

Uff, tylko jeden test, a jego uruchomienie trwa aż 3.58s.

A co się stanie kiedy zabraknie Internetu?

❯ rspec
F
Failures:1) Posts fetches all posts from external api service
Failure/Error: records = records + self.class.get(‘/posts’)
SocketError:
Failed to open TCP connection to jsonplaceholder.typicode.com:80 (getaddrinfo: nodename nor servname provided, or not known)
# — — Caused by: — -
# SocketError:
# getaddrinfo: nodename nor servname provided, or not known
# ./app/services/posts.rb:8:in `block in fetch’
Finished in 0.00527 seconds (files took 0.20419 seconds to load)
1 example, 1 failure

Test failuje. To znaczy, że nasz nasz CI również będzie. Zapewne wtedy, gdy akurat będziemy gasić hotfixem pożar na produkcji ;)

A więc VCR na ratunek!

Na dobry początek zainstaluj potrzebne biblioteki (dodatkowo instaluję webmock, którego działanie można porównać do strategii mockowania):

# Gemfilegroup :test do
gem ‘vcr’
gem ‘webmock’
end

Drugim krokiem będzie utworzenie helpera do testów:

# spec/vcr_setup.rbrequire ‘vcr’VCR.configure do |c|
c.cassette_library_dir = ‘spec/cassettes’
c.hook_into :webmock
c.allow_http_connections_when_no_cassette = true
end

Na końcu modifikujemy lekko nasz test:

# spec/services/posts_spec.rbrequire ‘rails_helper’
require ‘vcr_setup’
RSpec.describe Posts do
it ‘fetches all posts from external api service’ do
VCR.use_cassette(‘get posts’) do
expect(Posts.new.fetch.count).to eq(100*100)
end
end
end

I voilà. Uruchamiam test jeszcze raz:

❯ rspec
.
Finished in 4.04 seconds (files took 0.28721 seconds to load)
1 example, 0 failures

Tym razem, czas wykonywania jest dłuższy niż za pierwszym razem, ponieważ VCR nagrywa kasetę ;)

Ale następnym razem:

❯ rspec
.
Finished in 0.50465 seconds (files took 0.1583 seconds to load)
1 example, 0 failures

Z 3.58 sekundy przed skorzystaniem z VCR, teraz nasze testy wykonują się w 0.5 sekundy! To 7 razy szybciej!

Jeszcze jedna próba. Bez Internetu:

~/Projects/Ruby/vcr-example-app with-vcr*
❯ rspec
.
Finished in 0.61435 seconds (files took 0.17306 seconds to load)
1 example, 0 failures

Wciąż wszystko działa i w dodatku diabelsko szybko! :)

Możesz podejrzeć (a nawet ściągnąć i przetestować na swoim komputerze) jak wygląda całość w repozytorium github na branchu with-vcr

Oczywiście ten artykuł miał za zadanie tylko zachęcić Ciebie do korzystania z VCR w testach, a nie opisać wszystkie możliwe przypadki i konfiguracje. Jeśli przedstawiona powyżej konfiguracja nie jest wystarczająca dla Twoich potrzeb, polecam dokumentację na Relishapp.

Miłego testowania! :)

--

--