Mock - jak to działa :)

Cześć,
znalazłem kod który testuje czy logowanie się udało czy nie.
Chciałbym zrozumieć część dotyczącą sytuacji, gdzie użytkownik poda zły login lub hasło.
Szukam w googlu o mocku i let ale może mi to szybciej i lepiej podpowiecie.

let :invalid_admin do mock :admin, { :username => "invalid_username", :password => "s" } end
Jak działa powyższy kod? tworzy nowego admina ale go nie zapisuje?
Factory tworzy admina i go zapisuje i wtedy przy funkcji log_in username i password się niezgadzają?

[code=ruby]def log_in(admin)
visit ‘/zaloguj’
fill_in ‘username’, :with => admin.username
fill_in ‘password’, :with => “z”
click_button(‘Zaloguj’)
end
#######
feature ‘Sessions’ do
let :admin do
Factory :admin
end
let :invalid_admin do
mock :admin, { :username => “invalid_username”, :password => “s” }
end

context “User logs in” do
scenario ‘succesfully’, :focus do
log_in admin
page.should have_content ‘Zalogowano pomyślnie!’
current_path.should == root_path
end

scenario 'unsuccesfully', :focus do
  log_in invalid_admin 
  page.should have_content 'Źle wpisałeś nazwę lub hasło!'
  current_path.should == sessions_path
end

end
end[/code]

http://en.wikipedia.org/wiki/Mock_object :wink:

Mock to “sztuczny” obiekt, który modelujesz tak, żeby zwracał Ci metody potrzebne do przejścia testu. Np. jak chcesz przetestować metodę, która robi coś z tytułem artykułu, to niekoniecznie potrzebujesz prawdziwego artykułu w bazie danych, bo jego stworzenie zajmuje sporo czasu, wystarczy Ci obiekt, który będzie zachowywał się jak artykuł.

Np.:

[code]def upcased_title(article)
article.title.upcase
end

article = mock :article, :title => “hello”
upcased_title(article).should == “HELLO”[/code]
Mocki są mało popularne w środowisku rubiego, pewnie też trochę dlatego, że trudno jest to robić dobrze, ale sytuacja się trochę zmienia.

http://confreaks.net/videos/659-rubyconf2011-why-you-don-t-get-mock-objects
http://confreaks.net/videos/641-gogaruco2011-fast-rails-tests

Zarąbista prezentacja w temacie:
http://confreaks.net/videos/697-rubyconf2011-your-tests-are-lying-to-you

Wraz z polecanymi w niej rzeczami:

Różnica Mock vs Stub (MUST READ!!!):

Dzięki, już się zabieram do przeglądania reszty, a w międzyczasie pytanie, to dlaczego scenariusz ‘unsuccesfully’ działa? Bo jeżeli nie zapisuje do bazy danych to w sumie ten test jest chyba bez sensu.

W skrócie: tworzysz mock, wypisujesz oczekiwania wobec niego (czyli jakie ma wywołać metody i co te metody mają zwrócić), a test sprawdza, czy metody zostały wywołane, oraz jaką wartość zwróciły. Jeżeli metoda nie zostanie wywołana, choć oczekujemy tego - wówczas zostanie zwrócony błąd (przynajmniej tak to działa w rspec).

Pytanie do wtajemniczonych - czy zastępujecie mocki normalnymi obiektami w momencie gdy dodacie je do projektu? Chodzi mi o to, że jeżeli robicie mock na artykuł dla użytkownika, a następnie dodajcie model artykuł, czy wtedy automatycznie w testach również mock zamieniacie na obiekty modelu?

Pozdrawiam : )

no to trochę bez sensu… to tak jakby ten kod miał cokolwiek testować:

it "checks if foo is true" do foo = true foo.should be_true end
:smiley:

W takim razie jak powinienem przetestować scenariusz kiedy ktoś poda złe hasło i nie chcę tego robić za pomocą fill_in :username, :with => “zly_login”, jak takie scenariusze testujecie? Bo jest kilka możliwości i chcę wybrać najlepszą dla siebie :stuck_out_tongue:

Ale o jakich testach mówisz? jednostkowych czy integracyjnych? Bo jeżeli o integracyjnych to ta metoda jest chyba jedyna (i rzeczywiście integracyjna). Jeżeli o jednostkowych to zależy od Twojego kodu :wink:

Zakładając że używasz has_secure_password:

[code=ruby]let(:user) { Factory :user}

it “should authenticate user” do
User.find_by_username(user.username).authenticate(user.password).should be_true
end

it “should not authenticate user with non matching password” do
User.find_by_username(user.username).authenticate(‘inne_haslo’).should_not be_true
end

it “should not authenticate user with non matchng username” do
User.find_by_username(‘inny_user’).authenticate(user.password).should_not be_true
end

it “should not authenticate with both wrong data” do
User.find_by_username(‘inny_user’).authenticate(‘inne_haslo’).should_not be_true
end[/code]

zlw:

[code]it “logs a message on generate()” do
customer = stub(‘customer’, :name => ‘Aslak’)
logger = mock(‘logger’)
statement = Statement.new(customer, logger)

logger.should_receive(:log).with(/Statement generate for Aslak/)

statement.generate
end[/code]
Przykład z książki o RSpec. Jeżeli statement.generate nie wywoła log dla logger i nie zwróci danej wartości, to zostanie wyrzucony błąd.

no to trochę bez sensu… to tak jakby ten kod miał cokolwiek testować:[/quote]
Warto byłoby trochę poczytać zanim zaczynasz krytykować :wink: Pogramowanie obiektowe opiera się na przesyłaniu komunikatów, więc testowanie wywołania metod jest dobrą techniką. Inna sprawa jest taka, że niektórzy wolą prawdziwe obiekty - odwieczny spór mockists vs classicists, kwestia podejścia.

mniej więcej (pewnie mniej niż więcej) wiem jak działają mocki, ale akurat przykłady z którymi się spotkałem wyglądały jakby właściwie nic nie testowały :smiley:

Pytanko [w kodzie, w komentarzu] (może to mi coś rozjaśni). Załóżmy, że mamy model User, który ma pole #role. Mamy metody, które sprawdzają role - #admin?, #moderator?, #user?. Z tego co wiem, mockowanie wyglądałoby mniej więcej tak:

it "should check role - admin" do user = mock_model(User, role: :admin) user.should_receive(:admin?).and_return # no i cóż mam tu wstawić? kopię implementacji z modelu?! user.admin?.should be_true end
no chyba, że akurat do takiego przykładu nikt o zdrowych zmysłach nie użył by mockowanie :smiley:

it "should check role - admin" do user = User.new(role: :admin) user.admin?.should be_true end

  • powyższy kod bardziej do mnie przemawia i wygląda jakby rzeczywiście sprawdzał jak działa model. Ewentualnie, żeby nie ciągnąć za sobą załego railsowego ustrojstwa i nie spowalniać testów możemy zdefiniować klasę pomocniczą na potrzeby testu i za-includować do niej tylko potrzebny moduł:

[code=ruby]class DummyUser
include User::Role
end

it “should check role - admin” do
user = DummyUser.new
user.stub(:role).and_return(true)
user.admin?.should be_true
end[/code]

[quote]user.should_receive(:admin?).and_return # no i cóż mam tu wstawić? kopię implementacji z modelu?! user.admin?.should be_true
[/quote]
wczytaj się uważnie:
user powinien otrzymać (tj. wywołać) metodę admin?

user.should_receive(:admin?)

oraz
user powinien otrzymać metodę admin? i zwrócić true

user.should_receive(:admin?).and_return(true)

Widzisz w czym rzecz?
Teraz jeżeli nie wywoła metody (bądź nie zwróci jej z odpowiednią wartością) test wyrzuci błąd, bo naturalnie nie jest spełniony.

W zasadzie to przeczy temu, do czego mock ma służyć, bo skoro chcesz stworzyć wcześniej klasę, to na co mock?

Inaczej sprawa ma, jeżeli masz powiedzmy obiekt klasy A, który zawiera obiekty klas B i C. Idąc tym tokiem rozumowania zanim użyjesz A musisz mieć gotowe (i najlepiej przetestowane) klasy B i C. Natomiast jeżeli nie masz B i C, a mimo to chcesz przetestować A pod względem jego działania, możesz stworzyć mock B i C, ich metod które są używane w A, oraz tego co powinny zwrócić mockowane metody B i C, łapiesz ? ; )

Krótko mówiąc, mockowanie jest przydatne wtedy, gdy potrzebujesz użyć do przetestowania obiektów, których klas jeszcze nie stworzyłeś. Jeżeli już je masz, to raczej bez sensu.

[quote=Vayneyks]user.should_receive(:admin?).and_return(true)
Widzisz w czym rzecz?
Teraz jeżeli nie wywoła metody (bądź nie zwróci jej z odpowiednią wartością) test wyrzuci błąd, bo naturalnie nie jest spełniony.[/quote]
ten kod nie wywoła metody admin?. On tylko sprawdza czy została wykonana na user i jeśli tak to zwróci true. Metoda admin?może w ogóle nie istnieć

Moim zdaniem mockowanie ciągle ma sens. Jeśli metoda admin? potrzebuje 10 innych obiektów aby stwierdzić czy user jest adminem czy nie, a ty chcesz przetestować jakąć sytuację w której user jest adminem, to nie ma sensu przed kazdym testem trzworzenia tych 10 obiektow. Wystarczy zrobić stub na admin?.

Mockowanie ma kluczowe znaczenie np. w przypadku testowania kontrolerów.
Testowanie logiki kontrolerów nie wymaga inicjowania niekiedy całych drzew obiektów (wydajność!).
Przez stuby/mocki testujesz tylko to co powinieneś i ma to ogromne znaczenie dla szybkości testów, których przybywa.

Niewydajne testy => mniej testujesz => tworzysz gorszy kod => powrót do programowania !TDD.

Wywołując metodę sprawdzającą. Oczywiście masz rację :slight_smile:

Właściwie podzielam Twoje zdanie, chodziło mi natomiast o to, że jeżeli istnieje już klasa z metodą admin? to nie ma sensu robić na nią mock, tylko już jej użyć, a obiekty które potrzebuje można dalej mockować.

Właściwie podzielam Twoje zdanie, chodziło mi natomiast o to, że jeżeli istnieje już klasa z metodą admin? to nie ma sensu robić na nią mock, tylko już jej użyć, a obiekty które potrzebuje można dalej mockować.[/quote]
Wtedy bdziesz testował imlementację metody admin? w miejscu wktórym to nie jest potrzebne. I jak zmieni sie implentacja, to bedziesz musial zmieniac testy we wszystkich miejscach gdzie jej uzywasz.

Teraz łapię, dzięki : )