Использование musta для рефакторинга тестов rspec на моделях Rails

Изучив информацию о musta-matchers, ответив на другой вопрос Stackru о тестах доступности атрибутов (и подумав, что они довольно крутые), я решил попробовать рефакторинг тестов моделей, которые я делал в руководстве по Rails, чтобы сделать их еще более лаконичными и тщательными. Я сделал это благодаря некоторому вдохновению от документации для модулей Shoulda::Matchers::ActiveRecord а также Shoulda::Matchers::ActiveModel, а также этот ответ Stackru о структурировании обязательных тестов в моделях. Тем не менее, есть еще несколько вещей, в которых я не уверен, и мне интересно, как эти тесты можно сделать лучше.

В качестве примера я буду использовать спецификацию User в Rails Tutorial, поскольку она наиболее подробная и охватывает множество областей, которые можно улучшить. Следующий пример кода был изменен с исходного user_spec.rb и заменяет код вниз до describe "micropost associations" линия. Спецификация тестирует модель user.rb, и ее фабрика определена в фабриках.

спецификации / модель / user_spec.rb

# == Schema Information
#
# Table name: users
#
#  id              :integer          not null, primary key
#  name            :string(255)
#  email           :string(255)
#  created_at      :datetime         not null
#  updated_at      :datetime         not null
#  password_digest :string(255)
#  remember_token  :string(255)
#  admin           :boolean          default(FALSE)
#
# Indexes
#
#  index_users_on_email           (email) UNIQUE
#  index_users_on_remember_token  (remember_token)
#

require 'spec_helper'

describe User do

  let(:user) { FactoryGirl.create(:user) }

  subject { user }

  describe "database schema" do
    it { should have_db_column(:id).of_type(:integer)
                              .with_options(null: false) }
    it { should have_db_column(:name).of_type(:string) }
    it { should have_db_column(:email).of_type(:string) }
    it { should have_db_column(:created_at).of_type(:datetime)
                              .with_options(null: false) }
    it { should have_db_column(:updated_at).of_type(:datetime)
                              .with_options(null: false) }
    it { should have_db_column(:password_digest).of_type(:string) }
    it { should have_db_column(:remember_token).of_type(:string) }
    it { should have_db_column(:admin).of_type(:boolean)
                              .with_options(default: false) }
    it { should have_db_index(:email).unique(true) }
    it { should have_db_index(:remember_token) }
  end

  describe "associations" do
    it { should have_many(:microposts).dependent(:destroy) }
    it { should have_many(:relationships).dependent(:destroy) }
    it { should have_many(:followed_users).through(:relationships) }
    it { should have_many(:reverse_relationships).class_name("Relationship")
                         .dependent(:destroy) }
    it { should have_many(:followers).through(:reverse_relationships) }
  end

  describe "model attributes" do
    it { should respond_to(:name) }
    it { should respond_to(:email) }
    it { should respond_to(:password_digest) }
    it { should respond_to(:remember_token) }
    it { should respond_to(:admin) }
    it { should respond_to(:microposts) }
    it { should respond_to(:relationships) }
    it { should respond_to(:followed_users) }
    it { should respond_to(:reverse_relationships) }
    it { should respond_to(:followers) }
  end

  describe "virtual attributes and methods from has_secure_password" do
    it { should respond_to(:password) }
    it { should respond_to(:password_confirmation) }
    it { should respond_to(:authenticate) }
  end

  describe "accessible attributes" do
    it { should_not allow_mass_assignment_of(:password_digest) }
    it { should_not allow_mass_assignment_of(:remember_token) }
    it { should_not allow_mass_assignment_of(:admin) }
  end

  describe "instance methods" do
    it { should respond_to(:feed) }
    it { should respond_to(:following?) }
    it { should respond_to(:follow!) }
    it { should respond_to(:unfollow!) }
  end

  describe "initial state" do
    it { should be_valid }
    it { should_not be_admin }
    its(:remember_token) { should_not be_blank }
    its(:email) { should_not =~ /\p{Upper}/ }
  end

  describe "validations" do
    context "for name" do
      it { should validate_presence_of(:name) }
      it { should_not allow_value(" ").for(:name) }
      it { should ensure_length_of(:name).is_at_most(50) }
    end

    context "for email" do
      it { should validate_presence_of(:email) }
      it { should_not allow_value(" ").for(:email) }
      it { should validate_uniqueness_of(:email).case_insensitive }

      context "when email format is invalid" do
        addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
        addresses.each do |invalid_address|
          it { should_not allow_value(invalid_address).for(:email) }
        end
      end

      context "when email format is valid" do
        addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
        addresses.each do |valid_address|
          it { should allow_value(valid_address).for(:email) }
        end
      end
    end

    context "for password" do
      it { should ensure_length_of(:password).is_at_least(6) }
      it { should_not allow_value(" ").for(:password) }

      context "when password doesn't match confirmation" do
        it { should_not allow_value("mismatch").for(:password) }
      end
    end

    context "for password_confirmation" do
      it { should validate_presence_of(:password_confirmation) }
    end
  end

  # ...
end

Некоторые конкретные вопросы об этих тестах:

  1. Стоит ли вообще тестировать схему базы данных? В комментарии в ответе Stackru, упомянутом выше, говорится: "Я проверяю только те вещи, которые связаны с поведением, и я не рассматриваю наличие столбца или поведение индекса. Столбцы базы данных не просто исчезают, если кто-то намеренно их не удаляет, но вы Я могу с этим согласиться, но есть ли веская причина, по которой структура схемы базы данных будет проверена, и тем самым оправдать существование Shoulda::Matchers::ActiveRecord модуль? Возможно, стоит проверить только важные показатели...?
  2. Сделать should have_many тесты под "associations" заменить их соответствующими should respond_to тесты под "model attributes"? Я не могу сказать, является ли should have_many тест просто ищет соответствующий has_many объявление в файле модели или фактически выполняет ту же функцию, что и should respond_to,
  3. Есть ли у вас какие-либо другие комментарии / предложения, чтобы сделать эти тесты более краткими / читаемыми / тщательными, как по содержанию, так и по структуре?

5 ответов

Решение

1) Модуль Shoulda::Matchers::ActiveRecord содержит гораздо больше, чем просто сопоставления столбцов и индексов. Я бы немного покопался во включенных классах и посмотрел, что можно найти. Это где have_many, belong_to и т. д. Для отчета, однако, я вижу небольшую ценность в большей части того, что там находится.

2) Да, такие макросы, как have_many проверить гораздо больше, чем модель реагирует на метод. Из исходного кода вы можете увидеть, что именно он тестирует:

def matches?(subject)
  @subject = subject
  association_exists? &&
    macro_correct? &&
    foreign_key_exists? &&
    through_association_valid? &&
    dependent_correct? &&
    class_name_correct? &&
    order_correct? &&
    conditions_correct? &&
    join_table_exists? &&
    validate_correct?
end

3) Делать тесты более читабельными и / или лаконичными - это, безусловно, субъективный вопрос. Каждый ответит вам по-разному в зависимости от своего опыта и опыта. Я бы лично избавился от всего respond_to тесты и заменить их на тесты, которые имеют значение. Когда кто-то просматривает ваши тесты, он должен понимать общедоступный API для этого класса. Когда я вижу, что ваши объекты реагируют на что-то вроде "следующего?", Я могу делать предположения, но на самом деле не знаю, что это значит. Требуется ли аргумент? Возвращает ли оно логическое значение? Объект следует за чем-то или что-то следует за объектом?

Ваш вопрос затронул несколько моментов, я хотел бы ответить на два из них:

Ответ субъективен, поэтому я дам вам свое личное мнение.

1) Тестировать ActiveRecord таким образом?
Мой ответ - да. Вы могли бы написать сложные тесты с реальными данными, но если вы в основном доверяете ActiveRecord, вы можете сделать это таким образом, и если вы приступите к выполнению tdd, с этими тестами сначала они могут помочь в этом процессе.

2) Написать модель тестов вообще?
Мой ответ - да. Что я делаю, так это фокусирую контроллер и запрашиваю спецификации на удачном пути, а затем для случаев, когда требуются валидации и тому подобное, я пишу для них модульные тесты. Это оказалось хорошим разделением ответственности для меня.

Требования более низких уровней тестирования в основном исходят из вашей организации (внутренние документы), заказчик в основном предоставляет только спецификации требований заказчика (скажем, это самый высокий уровень в тестировании V-модели). Когда ваша организация начинает проектирование, sw шаг за шагом создает спецификации для более низких уровней тестирования.

Для вопроса "действительно ли мы нуждаемся в этом": это зависит от многих вещей: сложности приложения, критической безопасности или нет, стандартов, которым необходимо следовать, договорных / правовых / промышленных норм и т. Д.

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

Для "have_many and reply_to" я боюсь, что у меня нет справочной информации о том, как они реализованы, поэтому не могу ответить.

Я думаю, что все это следует рассматривать с точки зрения спецификации.

Если у вас есть спецификация уровня тестирования компонентов, которая охватывает необходимые столбцы базы данных для данной модели, в противном случае это не так.

Если это не предусмотрено, но, как ответственный разработчик, вы считаете, что это важно (ваш SW и его качественные характеристики лучше), вы должны договориться о включении этой информации в спецификацию, тогда вы можете поместить эти тесты в набор тестов.,

Я нашел некоторое значение в написании тестов на наличие столбцов базы данных. Вот почему:

1) Написание их держит меня в ритме TDD.
2) Миграции, как правило, довольно крутые, пока они не делают. Я знаю, что вы не должны редактировать существующую миграцию, но когда я сам над чем-то работаю, я иногда все равно делаю это. И если кто-то еще работает над тем же приложением и изменяет существующую миграцию вместо того, чтобы писать новую, эти тесты довольно быстро изолировали проблему для меня.

Если вы увязли в слишком большом количестве имен и типов столбцов, вы можете сделать что-то вроде этого, чтобы сэкономить, набрав:

describe User do

  describe 'database' do 

    describe 'columns' do 

      %w[reset_password_sent_at remember_created_at current_sign_in_at 
        last_sign_in_at confirmed_at confirmation_sent_at 
        created_at updated_at
        ].each do |column|
        it { should have_db_column(column.to_sym).of_type(:datetime) }
      end
    end

    describe 'indexes' do 

      %w[confirmation_token email reset_password_token
      ].each do |index|
        it { should have_db_index(index.to_sym).unique(true)}
      end
    end
  end  
end

Надеюсь, это поможет.

Другие вопросы по тегам