-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Part 6. Acceptance tests. #4
Conversation
… context in AnswersController spec.
…e feature 'user can create answer while being on question page'. Update AnswersController specs.
…date feature 'user can delete his questions but not another's'.
end | ||
|
||
def destroy | ||
@answer = Answer.find(params[:id]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В этом методе нет вьюх, поэтому можно обойтись исключительно локальными переменными.
|
||
def destroy | ||
@answer = Answer.find(params[:id]) | ||
@question = @answer.question |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А это зачем?
def destroy | ||
@answer = Answer.find(params[:id]) | ||
@question = @answer.question | ||
if @answer.user == current_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Во-первых, оформи это особым случаем.
Во-вторых, по коду часто встречается похожая проверка на авторство, оформи её методом.
@@ -34,8 +37,10 @@ def update | |||
end | |||
|
|||
def destroy | |||
@question.destroy | |||
redirect_to questions_path | |||
if @question.user == current_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут "особый случай" прямо просится.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Для изменения не нужна аналогичная проверка?
по логике -- да, по ТЗ -- пока нет) я думал над этим, но говорилось, что "нужно реализовывать только то, что было озвучено".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, моя ошибка — для изменения пока рано делать.
@@ -0,0 +1,10 @@ | |||
class User < ApplicationRecord | |||
has_many :questions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Что должно произойти со связанными вопросами и ответами при удалении пользователя?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
на мой, взгляж, ничего, просто вместо юзернейма должно стоять "ДЕЛЕТЕД ЮЗЕР". я не знаю, как именно это реализовано на стэковерфлоу, но по идее, вопросы и ответы -- это то, за чем приходят на ресурс. удаление юзеров -- это окей, удаление ключевого контента -- не очень. имхо.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Окей, главное что это мотивированное решение.
spec/features/question/show_spec.rb
Outdated
scenario 'unauthenticated user sees a question and answers' do | ||
visit question_path(question) | ||
|
||
expect(page).to have_content "#{"a" * 50}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут не очень понятно что за текст. Может expect(page).to have_content question.title
?
spec/features/question/show_spec.rb
Outdated
visit question_path(question) | ||
|
||
expect(page).to have_content "#{"a" * 50}" | ||
expect(page).to have_content "#{"b" * 50}", count: n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вот опять не понятно, если использовать question.answers.first.title
и так далее, то будет лучше.
spec/features/answer/create_spec.rb
Outdated
} do | ||
given(:question) { create(:question) } | ||
|
||
scenario 'unauthenticated user answers to a question' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не понял что за тест.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не понял что за тест.
Исправил имя сценария (забыл поменять после того, как отвечать на вопросы стало возможно только авторизованным пользователям).
spec/features/answer/create_spec.rb
Outdated
fill_in 'Body', with: "#{"body" * 25}" | ||
click_on 'Leave' | ||
|
||
expect(page).to have_content 'Your answer was saved.' |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
visit question_path(question1) | ||
click_on 'Delete the question' | ||
|
||
expect(page).to have_content "Your question was successfully deleted!" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ещё бы проверить что тест вопроса пропал.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ещё бы проверить что тест вопроса пропал.
Лучше добавлю проверку на отсутствие вопроса в БД -- текст вопроса при большом их кол-ве и многостраничной структуре отображения будет отсутствовать на главной не только после удаления вопроса.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Идея приёмочных тестов в том, что они работают максимально близко "к пользователю". То есть как будто это тестировщик в ручном режиме запустил браузер и кликает. В базу тестировщик обычно не добирается (ибо лень). То есть суть именно в проверке внешних эффектов, в этом смысле база данных — просто инструмент для получения нужного результата (текста на странице).
Я бы максимум проверил состояние базы в конце теста отдельными строками. Но, как правило, это лишнее.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Идея приёмочных тестов в том, что они работают максимально близко "к пользователю". То есть как будто это тестировщик в ручном режиме запустил браузер и кликает. В базу тестировщик обычно не добирается (ибо лень). То есть суть именно в проверке внешних эффектов, в этом смысле база данных — просто инструмент для получения нужного результата (текста на странице).
Я бы максимум проверил состояние базы в конце теста отдельными строками. Но, как правило, это лишнее.
Окей, логику понял. Убрал запрос в БД, сделал проверку контента.
spec/features/answer/create_spec.rb
Outdated
scenario 'answers to a question with errors' do | ||
click_on 'Leave' | ||
|
||
expect(page).to have_content 'Your answer was not saved.' |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
scenario 'asks a question with errors' do | ||
click_on 'Ask' | ||
|
||
expect(page).to have_content "Title can't be blank" |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сюда бы ещё вывод ошибки валидации.
Не очень понял. "Title can't be blank" - это ведь и есть одна из ошибок валидации?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, верно.
scenario "user tries to delete another's question" do | ||
visit question_path(question2) | ||
|
||
expect(page).to_not have_content 'Delete the question' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Наличие ссылок можно также проверять с помощью have_link
.
spec/features/question/index_spec.rb
Outdated
scenario 'unauthenticated user sees a list of questions' do | ||
visit questions_path | ||
|
||
expect(page).to have_content "Title of the question #1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Просится цикл.
…ibutes' 'creates answer by the name of logged user' test.
…y' 'tries to delete another user's answer' AnswerController tests.
… name of logged user' test.
…ionsController test.
…re tests of Answers.
it 'creates answer by the name of logged user' do | ||
post :create, params: { question_id: question.id, answer: attributes_for(:answer) } | ||
|
||
expect(assigns(:current_user)).to eq Answer.last.user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это не очень надёжная проверка: Answer.last
может вернуть непонятно что. Ещё тут нарушается принцип "чёрного ящика" для контроллера.
Одним из возможных решений может быть поискать ответ в ответах вопроса по атрибутам (https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find_by-21):
new_answer_params = attributes_for(:answer)
post :create, params: { question_id: question.id, answer: new_answer_params }
created_answer = question.answers.find_by! new_answer_params
expect(created_answer.user).to eq user
it 'creates a question by the name of logged user' do | ||
post :create, params: { question: attributes_for(:question) } | ||
|
||
expect(assigns(:current_user)).to eq question.reload.user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут две проблемы: нарушается принцип "чёрного ящика" для контроллера и усложняется модификация теста.
На усложнении модификации остановлюсь подробнее:
question.reload
expect(assigns(:current_user)).to eq question.user
в таком варианте можно дописывать новые проверки ниже, потом удалить исходную, и ничего не поломается.
@@ -0,0 +1,61 @@ | |||
require 'rails_helper' | |||
|
|||
feature 'user can create answer while being on question page', %q{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Этот тест дублирует следующий, насколько я понял.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Этот тест дублирует следующий, насколько я понял.
Как я понимаю, не совсем -- это же две разные фичи как бы: 1) юзер может отвечать прямо на странице вопроса 2) юзер может отвечать только если он залогинен.
Я понимаю, что там много одинакового кода, но фич как бы две и они поступили не одновременно, потому и в тестах двойное их отображение.
Как обычно поступают в таких случаях? Просто добавляют тест на вторую фичу (как в моём случае)? Объединяют две фичи в один фиче-тест? Выносят общий код в общую "предфичевую" зону? в таком случае, есть ли метод оператор, который предоставляет такое общее для двух фич пространство для кода?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В задании вот что:
- Пользователь, находясь на странице вопроса, может написать ответ на вопрос (т.е. форма нового ответа должна быть прямо на странице вопроса, без перехода на другую страницу)
...
- Реализовать следующие истоиии:
- Только аутентифицированный пользователь может создавать вопросы и ответы
При этом у нас есть противоречие — отвечают на вопрос только на странице вопроса и только в залогиненном состоянии. То есть если написать тест, то он одновременно проверит оба сценария из задания. И это верно.
Перечитаем второй сценарий:
- Только аутентифицированный пользователь может создавать вопросы и ответы
Хорошо, а как быть неаутентифицированным? Это и будет второй тест.
Итого: первым тестом проверяем что только залогиненный может ответить на вопрос на странице вопроса, вторым тестом проверяем что незалогиненный не имеет возможности ответить на странице вопроса.
end | ||
end | ||
|
||
def destroy | ||
answer = Answer.find(params[:id]) | ||
return if non_owned?(answer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Неплохо, но можно лучше.
Во-первых, не надо обратной логики, некоторым людям это взрывает мозг. return if non_owned?(answer)
--> return unless owned?(answer)
— и да, это единственный случай где уместен unless
.
Во-вторых, хочется аналогичную проверку использовать и во вьюшках. Ну и минимум кода написать. А значит можно завести метод в пользователе, который будет проверять, является ли пользователь автором переданного объекта.
В-третьих, в самом методе хочется избежать дополнительного запроса в базу. Хоть это и микрооптимизация, и, возможно, преждевременная, но тут полезная.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Неплохо, но можно лучше.
Во-первых, не надо обратной логики, некоторым людям это взрывает мозг.
return if non_owned?(answer)
-->return unless owned?(answer)
— и да, это единственный случай где уместенunless
.Во-вторых, хочется аналогичную проверку использовать и во вьюшках. Ну и минимум кода написать. А значит можно завести метод в пользователе, который будет проверять, является ли пользователь автором переданного объекта.
В-третьих, в самом методе хочется избежать дополнительного запроса в базу. Хоть это и микрооптимизация, и, возможно, преждевременная, но тут полезная.
Да, я про unless подумал в какой-то момент, но в тот момент правил что-то другое, а потом вернуться забыл)
Метод в модель перенёс, но насчёт избежать допзапроса в БД -- не понимаю, как реализовать. Проверка происходит в методе модели, объект проверки (вопрос, ответ и т.д.) передаётся в метод в качестве аргумента, до того в модели в инстанс-переменной этот объект закэширован не был -- как в модели избежать запроса на создателя объекта?
Вот этот запрос в логах:
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/models/user.rb:12
(0.1ms) BEGIN
↳ app/controllers/answers_controller.rb:24
Answer Destroy (0.9ms) DELETE FROM "answers" WHERE "answers"."id" = $1 [["id", 3]]
↳ app/controllers/answers_controller.rb:24
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Например вот так:
def author_of?(resource)
resource.user_id == id
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Например вот так:
def author_of?(resource) resource.user_id == id end
У меня пока вот так:
def owned?(content)
self == content.user
end
Не очень понимаю. По user_id -- не будет запроса в базу, но по user -- будет? В чём логика? Модель предоставляет ассоциативные методы (в данном случае user), сам объект вопроса/ответа в памяти есть -- но при запросе айди запроса в базу нет, при запросе ассоциации -- запрос в базу есть? По ассоциации всегда запрос в базу, так? По объекту из памяти -- соответственно, нет? Просто обращение к памяти?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Например вот так:
def author_of?(resource) resource.user_id == id end
Да, стало на один запрос меньше) Прикольно, спасибо.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
По resource.user_id
запроса в базу не будет, потому что тип у этого поля — число. И когда загружен из базы например вопрос, то соответственно загружены все поля строки вопроса, то есть и наше число для resource.user_id
.
По resource.user
запрос будет, потому что такого поля нет, а ассоциация belongs_to
в user
есть.
scenario 'asks a question with errors' do | ||
click_on 'Ask' | ||
|
||
expect(page).to have_content "Title can't be blank" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, верно.
visit question_path(question1) | ||
click_on 'Delete the question' | ||
|
||
expect(page).to have_content "Your question was successfully deleted!" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Идея приёмочных тестов в том, что они работают максимально близко "к пользователю". То есть как будто это тестировщик в ручном режиме запустил браузер и кликает. В базу тестировщик обычно не добирается (ибо лень). То есть суть именно в проверке внешних эффектов, в этом смысле база данных — просто инструмент для получения нужного результата (текста на странице).
Я бы максимум проверил состояние базы в конце теста отдельными строками. Но, как правило, это лишнее.
else | ||
render :new | ||
remember_answer_errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут должно появится чувство, что что-то не так. Слишком много рукопашки началось.
А что если тут отрендерить страницу показа вопроса? Позволит ли это использовать стандартных механизм отображения ошибок?
…name of logged user' AnswerController test.
…name of logged user' AnswerController test.
…the name of logged user' QuestionController test.
… and 'only authenticated user can create answers'.
end | ||
end | ||
|
||
def destroy | ||
answer = Answer.find(params[:id]) | ||
return unless current_user.owned?(answer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
тут лучше статуст какой-то вернуть (например head :forbidden
), потому что экшн должен дать какой-то ответ на запрос, нельзя просто из метода выйти. иначе тут будет попытка отрендерить вид destroy, которого нет и будет ошибка.
@@ -34,8 +37,9 @@ def update | |||
end | |||
|
|||
def destroy | |||
return unless current_user.owned?(@question) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
тут тоже нужно вернуть статус
app/views/answers/_answer.html.slim
Outdated
@@ -0,0 +1,2 @@ | |||
p= answer.body | |||
p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) if answer.user == current_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
используй метод User#owned?
везде, где нужна проверка на автора
app/views/questions/show.html.slim
Outdated
@@ -0,0 +1,12 @@ | |||
= render 'shared/errors', resource: @answer | |||
|
|||
=render(partial: 'shared/manage_question_links', locals: { question: @question }) if @question.user == current_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
используй метод User#owned?
@@ -0,0 +1,9 @@ | |||
require 'rails_helper' | |||
|
|||
RSpec.describe User, type: :model do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А где тесты модели на метод User#owned?
? Любой код, который ты пишешь в каком-либо классе должен иметь соответсвующие тесты
spec/models/user_spec.rb
Outdated
let(:question2) { create(:question, user: user2) } | ||
|
||
it 'check if user is owner of resource created by himself' do | ||
expect(user1.owned?(question1)).to be_truthy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А ещё лучше так: expect(user1).to be_owned(question1)
https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/predicate-matchers
No description provided.