How does UX transform businesses? The effects of usability testing
It has already become clear that User Experience (UX) is a key factor in shaping how companies build a business. The customer-centric approach has almost completely replaced […]
Kurs Ruby on Rails – Lekcja 8 – Testy
Testy są skryptami, które mają za zadanie sprawdzić, czy nasz kod robi dokładnie to, co powinien, ostrzec przed lukami w logice i sprawdzić poprawność współpracy między elementami systemu.
Pisanie testów pozwala na wcześniejsze wykrycie błędów. Inną mocną stroną posiadania i dbania o testy jest zmniejszenie ryzyka wystąpienia błędów regresyjnych podczas refaktoryzacji aplikacji. Czasem zdarza się, że dany kawałek kodu działa zbyt wolno i wymaga zmian optymalizacyjnych, natomiast nie zmienia się zasada działania całej funkcjonalności. W takim wypadku test, który wykaże, że po zmianach w kodzie nadal wszystko działa lub wręcz pokaże co nie działa, jest bardzo pomocny i zwiększa poczucie bezpieczeństwa.
Wybiegając trochę do przodu, będziemy używać biblioteki RSpec, która swoją składanią jednocześnie sprawia, że testy stają się pewnego rodzaju dokumentacją.
Generując projekt (rails new) nie podaliśmy parametru -T, przez co wygenerował nam się folder test dla domyślnego zestawu testowego jakim jest Minitest. Jako że z powyższych powodów zamierzamy korzystać z RSpec’a, usuńmy ten katalog. Następnie usuńmy gema poprzez:
gem cleanup minitest
Następnie dodajemy gemy rspec-rails, factory_bot_rails, oraz rails-controller-testing do naszego gemfile’u.
group :development, :test do
gem 'rspec-rails', '~> 5.0.0'
gem ‘factory_bot_rails’
gem ‘rails-controller-testing’
end
W terminalu w katalogu naszego projektu wklejamy następujące polecenia:
$ bundle install
$ rails generate rspec:install
Zainstaluje to nowe gemy oraz wygeneruje niezbędne pliki.
Factory bot jest przydatnym przy testowaniu gemem, który pozwoli nam przygotować faktorie, czyli bazowe dane, których będziemy używać do testów. Zastąpi nam to fixtures – czyli inny sposób organizacji danych testowych, używający plików yml.
Dodajemy do pliku spec/spec_helper.rb require 'factory_bot'
na samej górze pliku oraz config.include FactoryBot::Syntax::Methods
zaraz pod RSpec.configure do |config|
Testy, które odnoszą się do niewielkich wyizolowanych fragmentów kodu. Zazwyczaj testy pojedynczych metod, przykładowo metody modelu.
Testy, które sprawdzają poprawność kooperacji dwóch lub większej ilości modułów.
Zautomatyzowane testy, które sprawdzają flow większej konkretnej funkcjonalności z punktu widzenia użytkownika. Nie będziemy ich omawiać w tym kursie.
To, co najważniejsze w lekcji, czyli jak pisać testy. Przejdziemy przez cały proces na podstawie testu serwisu sort.rb
Na początek, wiemy jakie dane przyjmuje nasz serwis, stwórzmy ich szablony dla danych testowych.
Tworzymy plik post.rb
w katalogu spec/factories
. Umieszczamy w nim:
FactoryBot.define do
factory :post do
title { 'Post title' }
body { 'Lorem ipsum' }
user { create(:user) }
end
end
Tak zapisana faktoria sama zgadnie, że chodzi o model Post, kolejnym atrybutom przypisujemy domyślne wartości. Zwróćmy uwagę na asocjacje do modelu User. Mamy tu użytą metodę create z biblioteki FactoryBot
. Przekazujemy do niej jakiej faktorii chcemy użyć, w tym wypadku :user
. Taki zapis, w trakcie tworzenia obiektu post, stworzy i przypisze również obiekt User. Potrzebujemy więc faktorii dla klasy User. W tym samym katalogu tworzymy user.rb
z takim kodem:
FactoryBot.define do
factory :user do
name { 'Gal Anonim' }
end
end
Następnie tworzymy plik w folderze spec/services
o nazwie sort_spec.rb
. Jest to istotne aby nasze testy miały nazwę oryginalnego pliku z sufiksem _spec.rb
Zaczynamy nasz plik od dodania require 'rails_helper'
Po czym wstawiamy describe, nazwę naszej klasy i otwieramy blok. Wszystko co dotyczy tego testu będzie się znajdować w tym bloku.
describe Posts::Sort do
end
Przez to, że pierwszym argumentem naszego przypadku testowego jest nazwa klasy, mamy do dyspozycji zmienną described_class
, która zwróci nam Posts::Sort.
Poprzez metodę describe()
definiowany jest opis, czego dotyczy dany test. Opiszemy metodę call:
describe '#call' do
end
Następnie, dla porządku, możemy przypisać testowany przez nas obiekt do specjalnej zmiennej subject.
subject { described_class.new(posts).call }
Serwis sort.rb
przyjmuje jako argument kolekcję postów, stąd w metodzie new przekazana jest zmienna posts.
Załóżmy, że chcemy wyróżnić dwa przypadki, kiedy posts jest pustą kolekcją oraz kiedy zawiera 2 obiekty klasy Post.
context()
określa konkretny przypadek, który jest sprawdzany. Konteksty można swobodnie zagnieżdżać wiele razy. Rozpiszemy pierwszy przypadek pod zmienną subject:
context 'without posts provided' do
let(:posts) { Post.all }
end
Pojawiła się metoda let przyjmująca nazwę oraz wartość, która deklaruje elementy potrzebne do testów. Wartością są wszystkie dostępne w bazie posty. Do testów powinniśmy używać osobnej bazy danych. Tak też jest w naszym wypadku. Jeśli zajrzycie do pliku database.yml w folderze config, zobaczycie że wyróżnione są trzy bazy danych. Jedną z nich jest baza dla testów.
Metoda it()
tworzy cały test i określa jaki jest oczekiwany wynik. Dobrą praktyką jest pojedyncza odpowiedzialnośc testu, jeden test (it) powinien zawierać jedną asercję. Jeśli mamy ich więcej, bardzo możliwe, że powinniśmy rozdzielić to na kilka testow.
Nie stworzyliśmy w teście żadnego postu, więc przekazujemy pusta kolekcję. W takim wypadku spodziewamy się, że wywołanie serwisu również zwróci nam pustą kolekcję.
it 'returns empty collection' do
expect(subject).to eq([])
end
Czas na drugi przypadek, tworzymy nowy kontekst tym razem opisujący sytuację z podanymi postami.
context 'with posts provided' do
let!(:post_1) { create(:post, updated_at: Time.zone.now - 1.minutes) }
let!(:post_2) { create(:post, updated_at: Time.zone.now - 5.minutes) }
let(:posts) { Post.all }
end
Tak jak w przy tworzeniu faktorii Post
, używamy metody create
. Tym razem podajemy jej :post
jako szablon, z którego chcemy skorzystać. Wiemy, że wynik testu ma być posortowaną po atrybucie updated_at
kolekcją obiektów. Aby mieć nad tym kontrolę nadpiszemy ten atrybut dla dwóch tworzonych obiektów. Jeden będzie zaktualizowany 1 minutę temu, a drugi 5 minut temu.
it 'returns posts collection ordered by updated at' do
expect(subject).to eq([post_1, post_2])
end
Przewidujemy że nasz serwis uruchomiony z zadanymi parametrami zwróci odpowiednio posortowane dane.
Tak powinien wyglądać cały plik:
require 'rails_helper'
describe Posts::Sort do
describe '#call' do
subject { described_class.new(posts).call }
context 'without posts provided' do
let(:posts) { Post.all }
it 'returns empty collection' do
expect(subject).to eq([])
end
end
context 'with posts provided' do
let!(:post_1) { create(:post, updated_at: Time.zone.now - 1.minutes) }
let!(:post_2) { create(:post, updated_at: Time.zone.now - 5.minute) }
let(:posts) { Post.all }
it 'returns posts collection ordered by updated at' do
expect(subject).to eq([post_1, post_2])
end
end
end
end
Wspominaliśmy o dokumentacji, widzimy, że poszczególne elementy prowadzące do napisania testu, składają się w prosty angielski opisujący daną metodę.
Posts::Sort#call without posts provided returns empty array
orazPosts::Sort#call with posts provided returns posts ordered by updated at
Aby wywołać testy, wpisujemy w konsoli
bundle exec rspec
Widzimy, że jeden z testów zakończył się niepowodzeniem
Raport pokazuje nam plik oraz numer linijki z testem który nie przeszedł, mamy też opis przypadku, który był testowany oraz jaki błąd został wychwycony.
Poprawiamy kolejność postów w teście zamieniając expect(data).to eql([post_1, post_2])
na expect(data).to eql([post_2, post_1])
i uruchamiamy testy ponownie.
Tym razem widzimy, że wszystkie testy są zielone.
Testy modeli znajdują się w katalogu /spec/models, a nazywają się jak testowany obiekt z sufiksem _spec. Na ten moment nasze modele mogą jedynie testować asocjacje, walidacje oraz scope’y, dopiszmy więc do modelu Post jeszcze dwie metody. Metodę instancji:
def capitalized_title
title.capitalize
end
oraz metodę klasy:
def self.titles
Post.all.pluck(:title)
end
Następnie przechodzimy do testu, zwróćcie uwagę, że dodajemy type: :model, który zapewni nam odpowiednie metody pomocnicze.
require 'rails_helper'
describe Post, type: :model do
context 'relations' do
it 'belongs to user' do
expect(Post.reflect_on_association(:user).macro).to match(:belongs_to)
end
end
context 'validations' do
let(:user) { create(:user) }
it 'not valid without title' do
expect(Post.new(title: nil, body: 'body', user: user)).not_to be_valid
end
it 'not valid without body' do
expect(Post.new(title: 'title', body: nil, user: user)).not_to be_valid
end
it 'valid with title and body' do
expect(Post.new(title: 'title', body: 'body', user: user)).to be_valid
end
end
describe '#capitalized_title' do
let(:post) { create(:post, title: 'to be capitalized') }
subject { post.capitalized_title }
it 'returns capitalized title' do
expect(subject).to eq('To be capitalized')
end
end
describe '.titles' do
let(:post_1) { create(:post) }
let(:post_2) { create(:post) }
subject { Post.titles }
it 'returns array of titles' do
expect(subject).to match_array([post_1.title, post_2.title])
end
end
end
Możemy sprawdzić, czy nasz model na pewno ma zapisaną odpowiednią relację do modelu, możemy również napisać testy do walidacji, gdzie spodziewamy się, że obiekt inicjalizowany z nieprawidłowymi zestawami parametrów nie będzie poprawny. Jeśli chodzi o metody które dodaliśmy, konwencja zakłada, że pisząc testy do metody instancji, czyli w naszym wypadku capitalized_title, oznaczamy taką metodę poprzez ‘describe #nazwametody’. Natomiast metodę klasy oznaczamy poprzez ‘describe .nazwametody’. Tak uruchomiony test powinien zwrócić błąd. Metoda let bez wykrzyknika nie zostanie wywołana do momentu jawnego użycia w kodzie. W tym wypadku subject wykonywany jest zanim użyjemy post_1 oraz post_2. Innymi słowy, obiekty klasy post nie zostaną stworzone zanim wykonamy testowane polecenie.
Aby temu zaradzić, zmieńmy let na let!, który jest ewaluowany przed każdym scenariuszem.
Testy kontrolerów znajdują się w katalogu /spec/controllers/. Nazywają się tak jak kontroler z sufiksem _spec. Znów dodajemy atrybut type, tym razem :controller.
require 'rails_helper'
describe UsersController, type: :controller do
describe 'GET #index' do
it 'shows all the users' do
get :index
expect(assigns(:users).to_a).to eq(User.all.to_a)
end
end
describe 'POST #create' do
subject { post :create, params: params }
context 'valid params' do
let(:params) { { user: { name: 'Name' } } }
it 'creates user' do
expect{ subject }.to change{ User.count }.by(1)
end
end
context 'invalid params' do
let(:params) { { user: { name: nil } } }
it 'doesnt create user' do
expect{ subject }.not_to change{ User.count }
end
end
end
end
Testy integracyjne kontrolerów opisywać będą kolejne endpointy, w przykładzie widzimy test dla index’u oraz dla akcji create kontrolera użytkowników (UsersController). Pierwszy test sprawdzi, czy odpowiednie wartości przypisane zostały pod odpowiednią zmienną, w momencie gdy metoda index jest zapytana metodą get. Drugi test sprawdzi, czy użytkownik zostanie stworzony w zależności od parametrów podanych w danym kontekście. Używamy do tego metody change, która opisuje, że blok kodu zmieni pewien mutowalny stan. W tym wypadku, że wywołanie subject (w bloku {}) zmieni lub nie zmieni liczby obiektów klasy User. Oczywiście to, co chcemy testować powinno mieć uzasadnienie i pokrywać faktyczną potrzebę. W naszym wypadku logika tworzenia użytkownika znajduje się w kontrolerze, jeśli byśmy ją przenieśli do warstwy abstrakcji commandów jako osobny plik, wtedy dobrym pomysłem byłoby napisać test jednostkowy dla tego pliku, a test kontrolera mógłby skupić się np. na sprawdzeniu statusu zapytania, albo czy renderowany jest odpowiedni widok.
https://guides.rubyonrails.org/testing.html
https://www.betterspecs.org/
https://github.com/rspec/rspec-rails
https://github.com/thoughtbot/factory_bot
https://relishapp.com/rspec/rspec-expectations/v/3-0/docs
Przeczytaj również o...
It has already become clear that User Experience (UX) is a key factor in shaping how companies build a business. The customer-centric approach has almost completely replaced […]
What if there was one simple method to find opportunities for improved design, uncover UX problems, and learn about your target users’ behavior and preferences? Would you […]