Использование toa для рефакторинга rspec-тестов на моделях Rails
Узнав о shoulda-matchers, ответив qaru.site/info/482125/... (и думая, что они были довольно удивительными), я решил попробовать рефакторинг тестов модели, которые я сделал в The Rails Tutorial, пытаясь сделать они еще более сжатые и тщательные. Я сделал это благодаря некоторому вдохновению из документации для модулей Shoulda::Matchers::ActiveRecord
и Shoulda::Matchers::ActiveModel
, а также qaru.site/info/482126/... о структурировании тестов в моделях. Тем не менее, все еще есть кое-что, о чем я не уверен, и мне интересно, как эти тесты могут быть улучшены.
Я использую спецификацию User в Rails Tutorial в качестве моего примера, поскольку он является самым подробным, и охватывает множество областей, которые можно было бы улучшить. Следующий пример кода был изменен с исходного user_spec.rb и заменяет код до describe "micropost associations"
line. Спектральные тесты против модели user.rb, а ее factory определяется в factories.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[[email protected],com user_at_foo.org [email protected]]
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[[email protected] [email protected] [email protected] [email protected]]
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
Некоторые конкретные вопросы об этих тестах:
- Стоит ли вообще тестировать схему базы данных? Комментарий в ответе qaru.site/info/482126/..., говорит: "Я проверяю только те вещи, которые связаны с поведением, и я не считаю наличие поведения столбца или индекса. столбцы не просто исчезают, если кто-то умышленно их не удаляет, но вы можете защитить их от проверки кода и доверия", с которым я согласен, но есть ли веская причина, по которой будет проверена структура схемы базы данных, и, следовательно, оправдывая существование модуля
Shoulda::Matchers::ActiveRecord
? Возможно, только важные индексы заслуживают тестирования...?
- Проведите тесты
should have_many
в "associations"
в соответствии с их тегами should respond_to
в разделе "model attributes"
? Я не могу сказать, ищет ли тест should have_many
только соответствующее объявление has_many
в файле модели или фактически выполняет ту же функцию, что и should respond_to
.
- Есть ли у вас какие-либо другие комментарии/предложения, чтобы сделать эти тесты более краткими/удобочитаемыми/тщательными, как по содержанию, так и по структуре?
Ответы
Ответ 1
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 для этого класса. Когда я вижу, что ваши объекты отвечают на что-то вроде "follow?", Я могу делать предположения, но не знаю, что это значит. Это аргумент? Возвращает ли это логическое значение? Объект, следующий за чем-то или что-то, что следует за объектом?
Ответ 2
Ваш вопрос коснулся нескольких моментов, я хотел бы обратиться к двум из них:
Ответ субъективен, поэтому я дам вам личное дело.
1) Проверьте ActiveRecord таким образом?
Мой ответ - да. Вы можете написать сложные тесты с реальными данными, но если вы в основном доверяете ActiveRecord, вы можете сделать это таким образом, и если вы доберетесь до выполнения tdd, сначала с этими тестами они смогут помочь в этом процессе.
2) Напиши тесты модели вообще?
Мой ответ - да. То, что я делаю, является контроллером фокусировки и спецификациями запросов на счастливом пути, а затем для случаев, когда требуются проверки и т.д. Я пишу для них тесты модели единицы. Это оказалось хорошим разделением ответственности за меня.
Ответ 3
Я нашел некоторое значение при написании тестов для наличия столбцов базы данных. Вот почему:
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
Надеюсь, что это поможет.
Ответ 4
Я думаю, что все это должно быть видно со спецификационной точки зрения.
Если у вас есть спецификация уровня компонента, которая охватывает необходимые столбцы базы данных для данной модели, в противном случае вы не должны.
Если вы не охвачены, но как ответственный разработчик, вы считаете важным иметь (ваш sw и его качественные характеристики лучше всего), вам нужно организовать включение этой информации в спецификацию, тогда вы можете поместить эти тесты в тестовый пакет.
Ответ 5
Более низкие требования к уровням тестирования в основном поступают из вашей организации (внутренние документы), клиент в основном предоставляет только спецификацию требований клиентов (скажем, это самый высокий уровень тестирования V-модели).
По мере того, как ваша организация начинает дизайн, sw создает более низкие уровни тестовых уровней шаг за шагом.
Для "действительно ли нам нужен этот" вопрос: это зависит от многих факторов: сложность приложения, критически важная для безопасности или нет, стандарты для соблюдения, договорные/юридические/промышленные правила и т.д.
В целом я бы сказал, что для правильного идеального требования к требованию, отвечающего за модульное тестирование, следует написать спецификацию уровня единицы измерения, и тестер должен реализовать тест на основе этой спецификации.
Для "have_many и reply_to" я боюсь, что у меня нет информации о том, как они реализованы, поэтому не могу ответить.