Definition Spec w Ruby – implementacja w RSpec
💡

Definition Spec w Ruby – implementacja w RSpec

Wyobraź sobie taką sytuację. Projekt w Ruby on Rails. Pracujecie z zespołem nad mega ważnym biznesowym featurem. Spięliście się w 3 dni. Wszystko śmiga rewelacyjnie. Oddajecie. Wchodzi na produkcję. Management otwiera Prosecco,Marketing odpala ad wordsy. Pełen sukces 🎉🎉🎉 🔥💪

Tymczasem, godzinę po kluczowym releasie, kolega z drugiego zespołu wrzuca feature, nad którym pracował 3 tygodnie. Feature mógłby odczekać jeszcze jeden dzień, skoro czekał już tyle czasu. No ale nic, leci na produkcję, bo QA był zajęty testowaniem mego kluczowego featura, a to przecież mało biznesowa zmiana, jakieś widoki dashboardów i zapytania, no nic się nie da zepsuć. Wleci od razu na produkcję, a co. Pipeliny przechodzą na zielono, ale…aplikacja leży, Puma nie wstaje. Fakap. Klienci dzwonią, support w panice, Prosecco wygazowuje.

Okazuje się, że jakimś cudem na produkcję wjechał kod, w którym w jednej z klas, na samym jej końcu pojawiła się mała, bezpańska, prawie nie zauważalna literka „s”. Prawdopodobnie kolega zapisywał plik, omsknął mu się palec i litera została zapisana. Na code review nikt nie zauważył, na QA nie miało kiedy wjechać, wytestowane i wyklikane lokalnie, a mimo wszystko wywaliło aplikację.

image

Brzmi dziwnie? Otóż nie, to case study z projektu, w którym pracuje mój znajomy. Rzeczywisty, konkretny fakt autentyczny, który się wydarzył naprawdę.

Zaraz, ale jakim prawem wszystko działało lokalnie, jakim prawem przeszły pipeliny skoro w kodzie był znak, który nigdy nie powinien się tam znaleźć.

Winne temu zajściu są trzy kwestie:

  • specyfika języków interpretowanych
  • specyfika Ruby on Rails
  • brak odpowiedniego, minimalnego pokrycia testami w projekcie

Specyfika języków intepretowanych

Rożnica między językami interpetowanymi a językami kompilowanymi polega na tym, że w językach intepretowanych kod jest wykonywany w locie, nie ma żadnej fazy buildu, która pozwalałaby kompilatorowi sprawdzić, czy projekt w ogóle się buduje. Co za tym idzie wszelkie literówki, syntax errory, nie zadeklarowane metody wyjdą na jaw dopiero w momencie, kiedy wykonywany będzie zanieczyszczony fragment kodu. Nikt nas wcześniej nie ostrzeże. Żodyn.

image

Specyfika Ruby on Rails

RoR posiada mechanizm lazy loading w trybie development i test. Oznacza to, że ładuje tylko te klasy, które potrzebne są do wykonania danej parti kodu. Dopiero w trybie production wczytywany jest cały kod aplikacji. Skutkuje to tym, że o ile nie ma błędu w kluczowych plikach RoR odpowiedzialnych za inicjalizowanie aplikacji, to Puma zawsze wstanie w środowisku developerskim, a testy, o ile nie dotykają zanieczyszczonego kodu, zawsze będą przechodzić. Aplikacja nie wstanie za to na produkcji. Puma nie wstanie, jeśli w kodzie jest syntax error, albo wykonywana jest metoda, która nie została zadeklarowana.

Brak minimalnego pokrycia testami

Tak się złożyło, że felerna klasa z omawianego przykładu nie miała napisanych testów.

image

Pozostałe testy nie dotykały tej klasy, dlatego stwarzały pozorne wrażenie, że nic nie zostało zepsute i jest kompatybilność wsteczna. Testy przeszły lokalnie, przeszły w pipelinach, a mimo wszystko był fakap.

Ponoć każdy programista i każda programistka pracowali przynajmniej w jednym projekcie, w którym usłyszeli jedną z trzech wariacji: „Nie piszemy testów, bo”

  • „…klient nie płaci za pisanie testów”
  • „…nie ma czasu, terminy gonią.”
  • „…to prosty feature, co tu testować.”

Kto nigdy nie miał takiego fakapu niech pierwszy rzuci kamień.

Jedną z moich ulubionych myśli, którą kieruję się w pracy jest „Know how is important but the most important is to know how not.”

No to teraz i ja i mój znajomy i jego koledzy z zespołu i Ty wiemy, już „how not”. To pora sformułować krótki Know How.

Jeśli nie piszesz testów, bo jest jakiś super ważny biznesowy powód (ta jasne, super ważny), to zawsze warto napisać jeden test, który w podobnych, jak powyższy, przypadkach ratuje tyłek.

Nazywam go definition spec.

describe MailingService do
  it 'is defined' do
    expect(described_class).not_to be nil
  end
end

Polega on na tym, że pierwszym testem, jaki piszę przy tworzeniu nowej klasy jest sprawdzenie, czy w ogóle taka klasa jest zdefiniowania, co wymusza jej załadowanie w trybie testowym. To są 4 linijki, a naprawdę pozwalają zapewnić minimum bezpieczeństwa i uchronić się od literówek. Definition spec w Ruby wydaje się być narzędziem skrojonym do tego problemu.

Zobacz:

image

Niby niewiele, ale jest zielony kolor i ja jestem jakiś taki spokojniejszy.

image

Utworzyłem sobie moduł, żeby nie klepać za każdym razem tego samego kodu. Możesz też skorzystać z własnego matchera. Oto przykłady.

Własny moduł:

# spec/support/test_helpers.rb
module TestHelpersdef definition_spec
    expect(described_class).not_to be nil
  end
end

# w rails_helper.rb
RSpec.configure do |config|
  config.include TestHelpers
end

#Potem w teście
describe PaymentService
  it 'is defined' do
    definition_specend
end

describe SomeClass do
  it 'is defined' do
    definition_specend
end

Rezultat testów dla red:

image

Rezultat testów dla green:

image

Zdefiniowanie własnego matchera

#spec/support/matchers.rb
require 'rspec/expectations'

RSpec::Matchers.define :be_defined do |expected|
  match do |actual|
    !actual.nil?
  end
end

# Potem w teście
describe PaymentService do
  it { is_expected.to be_defined }
end

describe SomeClass do
  it { is_expected.to be_defined }
end

Rezultat testów dla red:

image

Rezultat testów dla green:

image

W przypadku TDD gwarantuje motywacyjny strzał dopaminy. Piszesz definition spec. Czerwono. Dodajesz plik i definiujesz klasę. Zielono. I od razu chce się pracować dalej 😀

Jeśli chcesz i masz na to czas, możesz przetestować czy publiczne metody, z których chcesz korzystać są zdefiniowane. Bez mockowania, bez rozbudowanych scenariuszy. Minimalne pokrycie, które chroni przed trywialnymi fakapami.

Tak jak startupy mają pojęcie MPV – Minimum Viable Product, tak ja proponuję Ci MVC, ale rozumiane jako Minimally Verified Class. Coś takiego jest w stanie zapewnić Ci wprowadzenie definition spec w Ruby do Twojej rutyny.

Ktoś mądry ze świata Ruby (Andrzej Krzywda – szerszy kontekst wypowiedzi znajdziesz tutaj) powiedział, że pracuje w Ruby, bo lubi poprawiać literówki na produkcji.

Nie poprawiajmy literówek na produkcji, róbmy to wcześniej, dużo wcześniej.

Po dodaniu definition speca testy wywalają się, jeśli w testowanej klasie znajduje się jakaś niepożądana litera.

Pisz definition spec w Ruby, serio.

A może warto odświeżyć swoją wiedzę o modyfikatorach dostępu w Ruby?