Test funkcjonalny i "No route matches"

Witam,

Mam pewnie pewnie banalny problem, ale nie mogę go rozwiązać. W Google jest mnóstwo wpisów z tym problemem, ale żadne rozwiązanie nie działa albo mnie nie nakierowało.

test "should create new track when sign in" do assert_difference('Track.count') do post :create, :track => { :youtube => 'http://www.youtube.com/watch?v=KXQ2mFYHUJA'} end assert_redirected_to track_path(assigns(:track)) end
I zwraca mi błąd:

ActionController::RoutingError: No route matches {:action=>"show", :controller=>"tracks"}

W przeglądarce wszystko działa. Routes są ustawione chyba poprawnie:

tracks GET /tracks(.:format) tracks#index POST /tracks(.:format) tracks#create new_track GET /tracks/new(.:format) tracks#new edit_track GET /tracks/:id/edit(.:format) tracks#edit track GET /tracks/:id(.:format) tracks#show PUT /tracks/:id(.:format) tracks#update DELETE /tracks/:id(.:format) tracks#destroy
Test pisałem wzorując się na: http://guides.rubyonrails.org/testing.html#a-fuller-functional-test-example

Spróbuj assert_redirect_to assigns(:track)

Generalnie chodzi o to, że nie podajesz id obiektu, który ma byc wyświetlony. Jak podasz obiekt, na który ma być przekierowanie to to zadziała. Ale jak używasz track_path to nie wystarczy wrzucić obiekt jako argument, tylko wskazać że to ma być id: track_path(:id => assigns(:track))

Ani jedno ani drugie nie pomaga, niestety.

Gdy mam samo:

assert_redirected_to assigns(:track)

To zwraca:

Expected response to be a redirect to <http://test.host/tracks> but was a redirect to <http://test.host/tracks/980190963>

Zaś pomysł z track_path(:id => assigns(:track)) też nie idzie, efekt taki sam jak na początku (RoutingError).

I wiem, że nie podaję id obiektu - podczas prób i błędów do tego doszedłem. Ale nie mam bladego pojęcia, jak to id nowo utworzonego obiektu sensownie przekazać. Jedyne co mi przychodzi na myśl, to jakiś Track.last.id użyć, ale nie wydaje mi się to najlepszym rozwiązaniem.

A ten assigns(:track) to zwraca rzeczywiście ten zapisany w bazie obiekt? Może masz w kontrolerze coś w stylu @track = Track.new po zapisaniu? (Dziwaczny pomysł, ale możliwy). Generalnie jakbyś pokazał kontroler, to by było łatwiej coś powiedzieć.

To ID obiektu możesz zebrać przez zwykłe assigns(:track).id - assigns zwraca zmienną @track z kontrolera. Możesz robić coś takiego:

track = assigns(:track)
assert_equal "Foobar", track.name

… żeby sprawdzić, czy zapisany obiekt ma jakieś sensowne atrybuty. Chyba, że to nie o to ci chodziło :slight_smile:

No właśnie nie, nie zwraca. Gdy robiłem wcześniej dla testu @cos = assigns(:track), to @cos było nil.

Nie :stuck_out_tongue_winking_eye:

Proszę, taka testowa metoda (która naturalnie, działa):

track = Track.new_track(params[:track]) if track.valid? track.save redirect_to track_path(track) else redirect_to tracks_path end

No nie działa, bo assigns(:track) sprawdza zmienną instancji, tzn. @track, a nie zmienną lokalną.

Zauważ, że używasz zmiennej lokalnej track zamiast zmiennej instancji @track. Tradycyjnie ważne zmienne w kontrolerze tworzy się jako zmienne instancji, choć głównym powodem jest to, że zmienne instancji są kopiowane do widoków. W akcjach typu create, w których niektórzy nie renderują widoków, zmiennych instancji używać można np. właśnie po to, żeby w testach mogła je odnaleźć metoda assign.

Aaaa… Widzisz. Z przyzwyczajenia nie używam zmiennych instancji, kiedy nie chcę z niczego korzystać “z zewnątrz”. Takie przyzwyczajenie związane z bezpieczeństwem i trzymania prywatnych rzeczy nie dostępnych z zewnątrz.

Czyli w teście nie dostanę się nijak do ID utworzonego obiektu, ani celu redirect_to, bez zrobienia zmiennej instancji i dobrania się do niej?

Kiedyś, daaawno temu robiłem taki myk, że analizowałem ścieżkę redirecta i z niej wyciągałem ID obiektu. Zamiast robić assert_redirected_to dopasowywałem do regexpa wartość nagłówka ‘Location’. Ale to było naprawdę dawno… Albo jeszcze o niej nie wiedziałem, albo wtedy jeszcze nie istniała metoda assigns (choć mogłem ją pewnie zastąpić przez instance_variable_get). Czasami też zapisywałem ID w sesji - session[:last_created_id] = track.id` - ale raczej nie polecam już tych sposobów.

Trochę mnie dziwi, że nie chcesz w razie błędu wyrenderować widoku akcji new, gdzie mogły by się znaleźć szczegółowe opisy błędów. Wtedy użycie zmiennej instancji ma sens, a i możesz błąd zasygnalizować kodem HTTP:

[code=ruby] def create
@track = Track.new(params[:track], :as => :creating)

respond_to do |format|
  if @track.save
    format.html { redirect_to track_path(@track), :status => 303, :notice => t('track.notices.created_ok') }
  else
    format.html { render :action => "new", :status => :unprocessable_entity }
  end
end

end[/code]

Osobiście nie cierpię, gdy mam jakiś formularz na głównej stronie, który coś dodaje / zapisuje, i po kliknięciu go w przypadku błędów przenosi mnie gdzieś na inną stronę. Powinno mnie przekierować z powrotem na tą samą stronę wraz z błędami, jakie wystąpiły. Ale to może raczej moje osobiste odczucie, niż sensowny powód :wink:

Dzięki za pomoc, testy przeszły :slight_smile:

Osobiste odczucie autora to dobry i sensowny powód :smiley:

Mam jeszcze pytanie. Jak korzystam z Twojego pomysłu z statusami itd. To po wysłaniu formularzu przenosi mnie na stronę, gdzie jedyne co mam napisane, to “You are being redirected.” gdzie ostatnie słowo jest linkiem do właściwej strony.

Dlaczego tak się dzieje? Skąd to się wzięło? Mógłbyś wyjaśnić? :wink:

EDIT:
I szybkie pytanie dodatkowe. Czy w modelu widać current_user z Devise? :smiley:

Stawiałbym na to, że przeglądarka nie przepada za kodem 303 (choć bardzo by mnie to dziwiło). Rails domyślnie chyba przekierowują kodem 302 - mniej prawomyślny, ale może lepiej wspierany? Jakiej używasz?

A o Devise to ja już nic nie wiem :frowning: Zawsze kod do uwierzytelniania piszę sam.

Obecnie Chromium 18.0.1025.168 (Build 134367 Linux) Ubuntu 12.04.
Sprawdziłem na FireFoxie 12.0 i jest to samo.

Stawiałbym na to, że strona jest generowana przez Railsy. Ale logi:

Started POST "/tracks" for 127.0.0.1 at 2012-06-01 12:09:17 +0200 Processing by TracksController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"uiTEhGyHORC7gwQENWtH0YN/5qsilP8XOrgQBVlvrOM=", "track"=>{"link"=>"http://jakis.pl", "tag_list"=>"cokolwiek", "description"=>""}} User Load (1.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 ActsAsTaggableOn::Tag Load (0.9ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" IS NULL AND "taggings"."taggable_type" = 'Track' AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL) (0.3ms) begin transaction (0.2ms) rollback transaction (4.4ms) begin transaction (1.0ms) rollback transaction Redirected to http://localhost:3000/tracks SQL (476.2ms) UPDATE "users" SET "updated_at" = '2012-06-01 10:09:17.153594' WHERE "users"."id" = 1 Completed 422 Unprocessable Entity in 561ms (ActiveRecord: 484.1ms)

Sprawdzę metodą prób i błędów :wink:

Tu musi być jakiś błąd:

Redirected to http://localhost:3000/tracks SQL (476.2ms) UPDATE "users" SET "updated_at" = '2012-06-01 10:09:17.153594' WHERE "users"."id" = 1 Completed 422 Unprocessable Entity in 561ms (ActiveRecord: 484.1ms)
Bo kod 422 nie powoduje redirecta. Tylko kody 30x mają takie działanie.

Znaczy chodzi mi o to, że ta treść strony jest generowana przez metodę redirect_to, ale potem kod jest ustawiany zamiast na 300-coś, to na 422, więc przeglądarka dostając kod 422 nie jest zobowiązana (a nawet nie powinna) zawracać sobie głowy nagłówkiem ‘Location’, więc nie robi przekierowania tylko wyświetla takiego HTML-a jakiego dostała.

W metodzie redirect_to powinieneś używać tylko kodów z grupy 300.

No dobra. Ostatecznie skończyło się na renderowaniu akcji :new. Średnio mi się podoba to rozwiązanie, bo chciałbym całość formularzu i zwracanych błędy mieć na :index. Może później się tym pobawię. Dzięki za pomoc :slight_smile:

Cała przyjemność po mojej stronie :slight_smile:

Mam nadzieję, że widok ‘new’ renderowałeś z kodem 422, a nie 200? Bo strona błędu z kodem OK wygląda trochę jak stwierdzenie lekarza: “Wszystko w porządku. Pacjent zmarł.” :smiley:

Tak, 422 jest ustawiony.

Teraz nasunęło mi się kolejne pytanie.

Mam formularz do edycji pliku w tracks#edit a aktualizacja jest w tracks#update. Jeśli w tracks#update wyniknie błąd, renderuje wtedy tracks#edit z kodem błędu 422. Ale wtedy nie ma redirect_to tracks#edit, więc link w przeglądarce wygląda np tak: localhost/tracks/3 zamiast localhost/tracks/3/edit

Jak sobie z tym poradzić?

Bo naturalnym odruchem jest, że będąc na localhost/tracks/3/edit klikamy formularz, aplikacja mieli, i wraca na localhost/tracks/3/edit z błędami.

Wydaje mi się, że brak ‘…/edit’ w adresie nie jest problemem. Linku do strony z błędem się raczej nigdzie nie podaje, więc potrzebne są tylko linki do konkretnego zasobu (czyli /tracks/3) albo do strony z której zaczyna się edycję (czyli /tracks/3/edit).

Redirect do strony, która by miała wyświetlić błąd jest problematyczny, bo jak przekazać dane o błędzie? Przez bazę danych? Sesję? Po co przekazywać, skoro wystarczy wypisać (wyrenderować) błąd i formatkę do jego poprawienia od razu?

Zaletą renderowania strony “od razu” jest też to, że F5 pozwala wszystko wysłać “jeszcze raz”. Czasami jest to miłe. Jak dla mnie, to właśnie przekierowanie do edit jest dość nienaturalnym odruchem :wink: