Skip to content
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

Merged
merged 37 commits into from
Jun 21, 2019
Merged

Part 6. Acceptance tests. #4

merged 37 commits into from
Jun 21, 2019

Conversation

metacorn
Copy link
Owner

No description provided.

end

def destroy
@answer = Answer.find(params[:id])
Copy link

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
Copy link

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
Copy link

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут "особый случай" прямо просится.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для изменения не нужна аналогичная проверка?

по логике -- да, по ТЗ -- пока нет) я думал над этим, но говорилось, что "нужно реализовывать только то, что было озвучено".

Copy link

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Что должно произойти со связанными вопросами и ответами при удалении пользователя?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

на мой, взгляж, ничего, просто вместо юзернейма должно стоять "ДЕЛЕТЕД ЮЗЕР". я не знаю, как именно это реализовано на стэковерфлоу, но по идее, вопросы и ответы -- это то, за чем приходят на ресурс. удаление юзеров -- это окей, удаление ключевого контента -- не очень. имхо.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Окей, главное что это мотивированное решение.

scenario 'unauthenticated user sees a question and answers' do
visit question_path(question)

expect(page).to have_content "#{"a" * 50}"
Copy link

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?

visit question_path(question)

expect(page).to have_content "#{"a" * 50}"
expect(page).to have_content "#{"b" * 50}", count: n
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот опять не понятно, если использовать question.answers.first.title и так далее, то будет лучше.

} do
given(:question) { create(:question) }

scenario 'unauthenticated user answers to a question' do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не понял что за тест.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не понял что за тест.

Исправил имя сценария (забыл поменять после того, как отвечать на вопросы стало возможно только авторизованным пользователям).

fill_in 'Body', with: "#{"body" * 25}"
click_on 'Leave'

expect(page).to have_content 'Your answer was saved.'

This comment was marked as resolved.

visit question_path(question1)
click_on 'Delete the question'

expect(page).to have_content "Your question was successfully deleted!"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ещё бы проверить что тест вопроса пропал.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ещё бы проверить что тест вопроса пропал.

Лучше добавлю проверку на отсутствие вопроса в БД -- текст вопроса при большом их кол-ве и многостраничной структуре отображения будет отсутствовать на главной не только после удаления вопроса.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Идея приёмочных тестов в том, что они работают максимально близко "к пользователю". То есть как будто это тестировщик в ручном режиме запустил браузер и кликает. В базу тестировщик обычно не добирается (ибо лень). То есть суть именно в проверке внешних эффектов, в этом смысле база данных — просто инструмент для получения нужного результата (текста на странице).

Я бы максимум проверил состояние базы в конце теста отдельными строками. Но, как правило, это лишнее.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Идея приёмочных тестов в том, что они работают максимально близко "к пользователю". То есть как будто это тестировщик в ручном режиме запустил браузер и кликает. В базу тестировщик обычно не добирается (ибо лень). То есть суть именно в проверке внешних эффектов, в этом смысле база данных — просто инструмент для получения нужного результата (текста на странице).

Я бы максимум проверил состояние базы в конце теста отдельными строками. Но, как правило, это лишнее.

Окей, логику понял. Убрал запрос в БД, сделал проверку контента.

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.

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.

Copy link
Owner Author

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" - это ведь и есть одна из ошибок валидации?

Copy link

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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Наличие ссылок можно также проверять с помощью have_link.

scenario 'unauthenticated user sees a list of questions' do
visit questions_path

expect(page).to have_content "Title of the question #1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просится цикл.

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
Copy link

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
Copy link

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{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Этот тест дублирует следующий, насколько я понял.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Этот тест дублирует следующий, насколько я понял.

Как я понимаю, не совсем -- это же две разные фичи как бы: 1) юзер может отвечать прямо на странице вопроса 2) юзер может отвечать только если он залогинен.

Я понимаю, что там много одинакового кода, но фич как бы две и они поступили не одновременно, потому и в тестах двойное их отображение.

Как обычно поступают в таких случаях? Просто добавляют тест на вторую фичу (как в моём случае)? Объединяют две фичи в один фиче-тест? Выносят общий код в общую "предфичевую" зону? в таком случае, есть ли метод оператор, который предоставляет такое общее для двух фич пространство для кода?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В задании вот что:

  • Пользователь, находясь на странице вопроса, может написать ответ на вопрос (т.е. форма нового ответа должна быть прямо на странице вопроса, без перехода на другую страницу)
    ...
  1. Реализовать следующие истоиии:
  • Только аутентифицированный пользователь может создавать вопросы и ответы

При этом у нас есть противоречие — отвечают на вопрос только на странице вопроса и только в залогиненном состоянии. То есть если написать тест, то он одновременно проверит оба сценария из задания. И это верно.

Перечитаем второй сценарий:

  • Только аутентифицированный пользователь может создавать вопросы и ответы

Хорошо, а как быть неаутентифицированным? Это и будет второй тест.

Итого: первым тестом проверяем что только залогиненный может ответить на вопрос на странице вопроса, вторым тестом проверяем что незалогиненный не имеет возможности ответить на странице вопроса.

end
end

def destroy
answer = Answer.find(params[:id])
return if non_owned?(answer)
Copy link

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.

Во-вторых, хочется аналогичную проверку использовать и во вьюшках. Ну и минимум кода написать. А значит можно завести метод в пользователе, который будет проверять, является ли пользователь автором переданного объекта.

В-третьих, в самом методе хочется избежать дополнительного запроса в базу. Хоть это и микрооптимизация, и, возможно, преждевременная, но тут полезная.

Copy link
Owner Author

@metacorn metacorn Jun 20, 2019

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

Copy link

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

Copy link
Owner Author

@metacorn metacorn Jun 20, 2019

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), сам объект вопроса/ответа в памяти есть -- но при запросе айди запроса в базу нет, при запросе ассоциации -- запрос в базу есть? По ассоциации всегда запрос в базу, так? По объекту из памяти -- соответственно, нет? Просто обращение к памяти?

Copy link
Owner Author

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

Да, стало на один запрос меньше) Прикольно, спасибо.

Copy link

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"
Copy link

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!"
Copy link

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
Copy link

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 unless current_user.owned?(answer)

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут тоже нужно вернуть статус

@@ -0,0 +1,2 @@
p= answer.body
p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) if answer.user == current_user

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,12 @@
= render 'shared/errors', resource: @answer

=render(partial: 'shared/manage_question_links', locals: { question: @question }) if @question.user == current_user

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А где тесты модели на метод User#owned?? Любой код, который ты пишешь в каком-либо классе должен иметь соответсвующие тесты

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
Copy link

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

@metacorn metacorn merged commit d4bca0f into master Jun 21, 2019
@metacorn metacorn changed the title Part 5. Acceptance tests. Part 6. Acceptance tests. Jul 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants