Everyday Rails - 5日目

P.57までの課題

contacts_controller_spec.rbにテストを追加。

  describe 'PATCH hide_contact' do
    before do
      @contact = create(:contact)
    end

    it 'marks the contact as hidden' do
      patch :hide_contact, id: @contact
      expect(@contact.reload.hidden?).to be_truthy
    end

    it 'redirects to contacts#index' do
      patch :hide_contact, id: @contact
      expect(response).to redirect_to contacts_url
    end
  end

be_trueはRSpec3では使えなくなっているので、be_truthyを使うこと。

migrationを追加。

./bin/rails g migration add_hidden_contact
class AddHiddenContacts < ActiveRecord::Migration
  def change
    add_column :contacts, :hidden, :boolean
  end
end

migration実行後、routing設定。

  resources :contacts do
    patch :hide_contact, on: :member
  end

controller修正。before_actionは指定数的な問題でonlyからexceptに変えた。

# :
  before_action :set_contact, except: [:index, :new, :create]
# :
  def hide_contact
    @contact.update(hidden: true)
    redirect_to contacts_url
  end
# :

結果。
f:id:yossk:20141206143232j:plain

P.58の課題

phonesコントローラーを作る。

$ ./bin/rails g controller Phones show

routes.rb

  resources :contacts do
    patch :hide_contact, on: :member
    resources :phones
  end

phones_contoller_spec.rb

describe PhonesController, type: :controller do
  describe 'GET #show' do
    it 'renders the :show template for the phone' do
      contact = create(:contact)
      get :show, id: contact.phones.first, contact_id: contact.id
      expect(response).to render_template :show
    end
  end
end

P.60までの課題

CSVJSON出力について。

contacts_controller.rbにCSV出力とJSON出力を追加。

  def index
    if params[:letter]
      @contacts = Contact.by_letter(params[:letter])
    else
      @contacts = Contact.order('lastname, firstname')
    end

    respond_to do |format|
      format.html
      format.json { render json: @contacts.to_json }
      format.csv do
        send_data Contact.to_csv(@contacts),
          type: 'text/csv; charset=utf-8; header=present',
          disposition: 'attachment; filename=contacts.csv'
      end
    end
  end

contact.rb(Contactモデル)に特異メソッドto_csv追加。
とりあえずControllerのテスト用に適当に。
これはモデルにもちゃんとspec追加しないといけないかな(今回は省略)。

  def self.to_csv(contacts)
    contacts.inject('') do |csv, contact|
      csv << CSV.generate_line(
        [contact.firstname, contact.lastname, contact.email]
      )
    end
  end

contacts_controller_spec.rb

  describe 'GET #index' do
    # :

    context 'CSV output' do
      it 'return a CSV file' do
        get :index, format: :csv
        expect(response.headers['Content-Type']).to match 'text/csv'
      end

      it 'returns contact' do
        contact = create(:contact)
        get :index, format: :csv
        expect(response.body).to match(
          [contact.firstname, contact.lastname, contact.email].join(",")
        )
      end
    end

    context 'JSON output' do
      it 'returns JSON-formatted contact' do
        contact = create(:contact)
        get :index, format: :json
        expect(response.body).to have_content contact.to_json
      end
    end
  end

P.77まで

shared_examples, support module、custom matcherを用いて簡潔に。
現段階では、custom matcherよりもRSpec自体の使い方をマスターするほうが優先に感じた。

require 'rails_helper'

describe ContactsController, type: :controller do
  shared_examples 'public access to contacts' do
    before do
      @contact = create(:contact, firstname: 'Lawrence', lastname: 'Smith')
    end

    describe 'GET #index' do
      context 'with params[:letter]' do
        it 'populates an array of contacts starting with the letter' do
          jones = create(:contact, lastname: 'Jones')
          get :index, letter: 'S'
          expect(assigns(:contacts)).to match_array([@contact])
        end

        it 'renders the :index template' do
          get :index, letter: 'S'
          expect(response).to render_template :index
        end
      end

      context 'without params[:letter]' do
        it 'populates an array of all contacts' do
          jones = create(:contact, lastname: 'Jones')
          get :index
          expect(assigns(:contacts)).to match_array([@contact, jones])
        end

        it 'renders the :index template' do
          get :index
          expect(response).to render_template :index
        end
      end

      context 'CSV output' do
        it 'return a CSV file' do
          get :index, format: :csv
          expect(response.headers['Content-Type']).to match 'text/csv'
        end

        it 'returns contact' do
          get :index, format: :csv
          expect(response.body).to match(
            [@contact.firstname, @contact.lastname, @contact.email].join(",")
          )
        end
      end

      context 'JSON output' do
        it 'returns JSON-formatted contact' do
          get :index, format: :json
          expect(response.body).to have_content @contact.to_json
        end
      end
    end

    describe 'GET #show' do
      it 'assigns the requested contact to @contact' do
        get :show, id: @contact
        expect(assigns(:contact)).to eq @contact
      end

      it 'renders the :new template' do
        get :show, id: @contact
        expect(response).to render_template :show
      end
    end
  end

  shared_examples 'full access to contacts' do
    describe 'GET #new' do
      it 'assigns a new Contact to @contact' do
        get :new
        expect(assigns(:contact)).to be_a_new Contact
      end

      it 'renders the :new template' do
        get :new
        expect(response).to render_template :new
      end
    end

    describe 'GET #edit' do
      it 'assigns the requested contact to @contact' do
        contact = create(:contact)
        get :edit, id: contact
        expect(assigns(:contact)).to eq contact
      end

      it 'renders the :edit template' do
        contact = create(:contact)
        get :edit, id: contact
        expect(response).to render_template :edit
      end
    end

    describe 'POST #create' do
      before do
        @phones = [
          attributes_for(:phone),
          attributes_for(:phone),
          attributes_for(:phone),
        ]
      end

      context 'with valid attributes' do
        it 'save the new contact in the database' do
          expect {
            post :create,
              contact: attributes_for(:contact, phones_attributes: @phones)
          }.to change(Contact, :count).by(1)
        end

        it 'redirects to contacts#show' do
          post :create,
            contact: attributes_for(:contact, phones_attributes: @phones)
          expect(response).to redirect_to contact_url(assigns(:contact))
        end
      end

      context 'with invalid attributes' do
        it 'dose not save the new contact in the database' do
          expect {
            post :create, contact: attributes_for(:invalid_contact)
          }.not_to change(Contact, :count)
        end

        it 're-renders the :new template' do
          post :create, contact: attributes_for(:invalid_contact)
          expect(response).to render_template :new
        end
      end
    end

    describe 'PATCH #update' do
      before do
        @firstname = 'Larry'
        @lastname = 'Smith'
        @contact = create(:contact, firstname: @firstname, lastname: @lastname)
      end

      context 'with valid attributes' do
        it 'updates the contact in the database' do
          patch :update, id: @contact, contact: attributes_for(:contact)
          expect(assigns(:contact)).to eq @contact
        end

        it "changes @contact's attribetus" do
          firstname = 'Larry'
          lastname  = 'Wall'
          patch :update, id: @contact,
            contact: attributes_for(
              :contact, firstname: firstname, lastname: lastname
            )
          @contact.reload
          expect(@contact.firstname).to eq firstname
          expect(@contact.lastname).to eq lastname
        end

        it 'redirects to the contact' do
          patch :update, id: @contact, contact: attributes_for(:contact)
          expect(response).to redirect_to @contact
        end
      end

      context 'with invalid attributes' do
        it 'dose not update the contact' do
          firstname = 'David'
          patch :update, id: @contact,
            contact: attributes_for(:contact, firstname: firstname, lastname: nil)
          @contact.reload
          expect(@contact.firstname).not_to eq firstname
          expect(@contact.lastname).to eq @lastname
        end

        it 're-renders the :edit template' do
          patch :update, id: @contact, contact: attributes_for(:invalid_contact)
          expect(response).to render_template :edit
        end
      end
    end

    describe 'DELETE #destroy' do
      before do
        @contact = create(:contact)
      end

      it 'deletes the contact from the database' do
        expect {
          delete :destroy, id: @contact
        }.to change(Contact, :count).by(-1)
      end

      it 'redirects to contacts#index' do
        delete :destroy, id: @contact
        expect(response).to redirect_to contacts_url
      end
    end

    describe 'PATCH hide_contact' do
      before do
        @contact = create(:contact)
      end

      it 'marks the contact as hidden' do
        patch :hide_contact, id: @contact
        expect(@contact.reload.hidden?).to be_truthy
      end

      it 'redirects to contacts#index' do
        patch :hide_contact, id: @contact
        expect(response).to redirect_to contacts_url
      end
    end
  end

  describe 'Administrator access' do
    before do
      set_user_session create(:admin)
    end

    it_behaves_like 'public access to contacts'
    it_behaves_like 'full access to contacts'
  end

  describe 'Guest access' do
    it_behaves_like 'public access to contacts'

    describe 'GET #new' do
      it 'returns login' do
        get :new
        expect(response).to require_login
      end
    end

    describe 'GET #edit' do
      it 'requires login' do
        contact = create(:contact)
        get :edit, id: contact
        expect(response).to redirect_to login_url
      end
    end

    describe 'POST #create' do
      it 'requires login' do
        post :create, contact: attributes_for(:contact)
        expect(response).to redirect_to login_url
      end
    end

    describe 'PATCH #update' do
      it 'requires login' do
        patch :update, id: create(:contact),
          contact: attributes_for(:contact)
        expect(response).to redirect_to login_url
      end
    end

    describe 'DELETE #destroy' do
      it 'requires login' do
        delete :destroy, id: create(:contact)
        expect(response).to redirect_to login_url
      end
    end
  end
end

今日の感想

Railsはテストが非常に簡単に書けるな。
これは大変なメリットだと感じた。
フレームワークにテスト機構が組み込まれていると、テストを書くのにストレスがない。
テストを実行するのにもストレスがない。
いいね!