Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора? |
Микросообщения пользователей
Улучшение микросообщений
Тесты 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