Опубликован: 27.01.2016 | Доступ: свободный | Студентов: 914 / 58 | Длительность: 23:07:00
Лекция 10:

Микросообщения пользователей

< Лекция 9 || Лекция 10: 123456 || Лекция 11 >

Улучшение микросообщений

Тесты has_many ассоциации в Листинге 10.6 мало чего тестируют — они просто проверяют существование атрибута microposts. В этом разделе мы добавим упорядочивание и зависимость к микросообщениям, а также протестируем что user.microposts метод действительно возвращает массив микросообщений.

Нам нужно будет построить несколько микросообщений в тесте модели User, что означает, что мы должны сделать фабрику микросообщений в этой точке. Для этого нам нужен способ для создания ассоциации в Factory Girl. К счастью, это легко, как видно в Листинге 10.9.

FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"

    factory :admin do
      admin true
    end
  end

  factory :micropost do
    content "Lorem ipsum"
    user
  end
end
Листинг 10.9. Полный файл фабрики, включающий новую фабрику для микросообщений. spec/factories.rb

Здесь мы сообщаем Factory Girl о том что микросообщения связаны с пользователем просто включив пользователя в определение фабрики:

factory :micropost do
  content "Lorem ipsum"
  user
end

Как мы увидим в следующем разделе, это позволяет нам определить фабричные микросообщения следующим образом:

FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)

Дефолтное пространство (scope)

По умолчанию, использование user.microposts для вытягивания пользовательских микросообщений из базы данных не дает никаких гарантий сохранения порядка микросообщений, но мы хотим (следуя конвенции блогов и Twitter), чтобы микросообщения выдавались в обратном хронологическом порядке, т.е. последнее созданное сообщение должно быть первым в списке. Для проверки этого порядка мы сначала создаем пару микросообщений следующим образом:

FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)

Здесь мы указываем (использование временнЫх хелперов обсуждалось в Блоке 8.1) что второй пост был создан совсем недавно, т.e. 1.hour.ago (один.час.назад), в то время как первый пост был создан 1.day.ago (один.день.назад). Обратите внимание, насколько удобна Factory Girl в использовании: мы также можем установить created_at вручную, чего нам не позволяет делать Active Record. (Вспомните что created_at и updated_at являются "волшебными" столбцами, автоматически устанавливающими правильные временные метки создания и обновления, так что любая явная инициализация значений переписывается магическим образом.)

Большинство адаптеров баз данных (в том числе адаптер SQLite) возвращает микросообщения в порядке их id, поэтому мы можем организовать начальные тесты, которые почти наверняка провалятся, используя код в Листинге 10.10. Здесь используется метод let! (читается как "let bang") вместо let; причина его использования заключается в том, что переменные let являются ленивыми, а это означает что они рождаются только при обращении к ним. Проблема в том, что мы хотим чтобы микросообщения появились незамедлительно, так, чтобы временные метки были в правильном порядке и так, чтобы @user.microposts не было пустым. Мы достигаем этого с let!, который принуждает соответствующие переменные появляться незамедлительно.

require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end

    it "should have the right microposts in the right order" do
      expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
    end
  end
end
Листинг 10.10. Тестирование порядка микросообщений пользователя. spec/models/user_spec.rb

Ключевой строкой здесь является

expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]

указывающая, что сообщения должны быть упорядочены таким образом, чтобы новейшее сообщение было первым. Этот тест должен быть провальным, так как по умолчанию сообщения будут упорядочены по id, т.е., [older_micropost, newer_micropost]. Эти тесты также тестируют базовую корректность самой has_many ассоциации, проверяя (как указано в Таблице 10.1) что user.microposts является массивом микросообщений. Метод to_a, который мы обсуждали ранее в Разделе 4.3.1, конвертирует @user.microposts из их дефолтного состояния (которым оказывается является "collection proxy" из библиотеке Active Record) в массив подходящий для сравнения с тем что мы создали вручную.

Для того чтобы получить прохождение тестов упорядоченности, мы используем Rails средство default_scope с аргументом order, как показано в Листинге 10.11. (Это наш первый пример понятия пространства (scope). Мы узнаем о пространстве в более общем контексте в Главе 11.)

class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :user_id, presence: true
end
Листинг 10.11. Упорядочивание микросообщений с default_scope. app/models/micropost.rb

За порядок здесь отвечает ’created_at DESC’, где DESC это SQL для "по убыванию", т.е., в порядке убывания от новых к старым.

В Rails 4.0 все скоупы принимают анонимную функцию которая возвращает критерий нужный для данного скоупа, в основном таким образом что скоуп не должен быть оценен немедленно, но может быть загружен позже по запросу (так называемая ленивая оценка). В данном случае такой функцией является

-> { order('created_at DESC') }

Отличительным синтаксическим признаком таких объектов, называемых Proc (процедура) или lambda, является стрелка ->. Эти объекты принимают блок (Раздел 4.3.2), а затем оценивают его когда их вызывают методом call. Мы можем увидеть как это работает в консоли:

>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil

(Procs это довольно продвинутая Ruby-тема, так что не переживайте если не смогли уловить смысла в написанном выше.)

Dependent: destroy

Помимо правильного упорядочивания, есть второе уточнение, которое мы хотели бы добавить в микросообщения. Напомним из Раздела 9.4, что администраторы сайта имеют право уничтожать пользователей. Само собой разумеется, что если пользователь уничтожен, то должны быть уничтожены и его микросообщения. Мы можем протестировать это вначале уничтожив пользователя, а затем проверив, что связанных с ним микросообщений больше нет в базе данных.

Для того чтобы как следует протестировать удаление микросообщений нам вначале необходимо получить микросообщение данного пользователя в переменную, а затем удалить пользователя. Простая реализация выглядит примерно так:

microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
  # Make sure the micropost doesn't appear in the database.
end

Здесь вызов to_a создает копию микросообщений и мы включили строку

expect(microposts).not_to be_empty

для отлова ошибок которые могут возникнуть при случайном удалении to_a. Проблема в том, что, без to_a, удаление пользователя будет удалять сообщения в переменной microposts и, как результат

microposts.each do |micropost|
  # Make sure the micropost doesn't appear in the database.
end

вообще ничего не будет тестировать поскольку переменная microposts будет пустой.

Мы можем выразить ожидание того что микросообщения не появятся в базе данных следующим образом:

microposts.each do |micropost|
  expect(Micropost.where(id: micropost.id)).to be_empty
end

Здесь мы использовали Micropost.where, который возвращает пустой объект в случае если запись не найдена, в то время как Micropost.find в аналогичной ситуации вызовет исключение, что немного сложнее тестировать. (На случай если вам любопытно, код

expect do
  Micropost.find(micropost)
end.to raise_error(ActiveRecord::RecordNotFound)

проделывает этот трюк в данном случае.)

Собрав все вместе мы получаем код в Листинге 10.12.

require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    it "should destroy associated microposts" do
      microposts = @user.microposts.to_a
      @user.destroy
      expect(microposts).not_to be_empty
      microposts.each do |micropost|
        expect(Micropost.where(id: micropost.id)).to be_empty
      end
    end
  end
end
Листинг 10.12. Тестирование того, что микросообщения уничтожаются вместе с пользователями. spec/models/user_spec.rb

Код приложения, необходимый для прохождения тестов из Листинга 10.12 короче чем одна строка; в самом деле, это всего лишь опция метода ассоциации has_many, как показано в Листинге 10.13.

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  .
  .
  .
end
Листинг 10.13. Обеспечение уничтожения микросообщений пользователя вместе с пользователем. app/models/user.rb

Здесь опция dependent: :destroy в

has_many :microposts, dependent: :destroy

приговаривает связанные микросообщения (т.e., те что принадлежат данному пользователю) быть уничтоженными при уничтожении самого пользователя. Это предотвращает застревание в базе данных бесхозных микросообщений при удалении админами пользователей из системы.

В этом окончательном виде ассоциация пользователь/микросообщения готова к использованию и все тесты должны пройти:

$ bundle exec rspec spec/

Валидации контента

Прежде чем покинуть модель Micropost, мы добавим валидации для атрибута content (следуя примеру из Раздела 2.3.2). Как и user_id, атрибут content должен существовать, а его длина не должна превышать 140 символов, что сделает его настоящим микросообщением. Тесты в основном следуют примерам из тестов валидации модели User в Разделе 6.2, как это показано в Листинге 10.14.

require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before { @micropost = user.microposts.build(content: "Lorem ipsum") }
  .
  .
  .
  describe "when user_id is not present" do
    before { @micropost.user_id = nil }
    it { should_not be_valid }
  end

  describe "with blank content" do
    before { @micropost.content = " " }
    it { should_not be_valid }
  end

  describe "with content that is too long" do
    before { @micropost.content = "a" * 141 }
    it { should_not be_valid }
  end
end
Листинг 10.14. Тесты валидаций модели Micropost. spec/models/micropost_spec.rb

Как и в Разделе 6.2, код в Листинге 10.14 использует мультипликацию строк для тестирования валидации длины микросообщения:

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

Код приложения укладывается в одну строку:

validates :content, presence: true, length: { maximum: 140 }

Результирующая модель Micropost показана в Листинге 10.15.

class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :content, presence: true, length: { maximum: 140 }
  validates :user_id, presence: true
end
Листинг 10.15. Валидации модели Micropost. app/models/micropost.rb
< Лекция 9 || Лекция 10: 123456 || Лекция 11 >
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?