Статьи

Тестирование RSpec для начинающих, часть 3

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

  • Тест скорости
  • Узкие места в базе данных
  • Spring Preloader
  • Iffy RSpec Удобства
  • Тайные гости
  • Встроенный код
  • Методы извлечения

Теперь, когда у вас есть основы, мы должны уделить время, чтобы обсудить несколько сомнительных частей RSpec и TDD — несколько проблем, которые могут быть легко использованы чрезмерно, и некоторые недостатки использования частей DSpec RSpec без изменений. Я хочу избежать использования множества продвинутых концепций в вашем недавно появившемся мозге TDD, но я чувствую, что необходимо сделать несколько замечаний, прежде чем вы начнете свое первое испытание. Кроме того, создание медленного набора тестов из-за вредных привычек, которых легко избежать, — это то, что вы можете улучшить как новичок сразу.

Конечно, есть несколько вещей, с которыми вам нужно получить больше опыта, прежде чем вы будете чувствовать себя комфортно и эффективно с тестированием, но я уверен, что вы также почувствуете себя лучше с самого начала, если вы уберете некоторые из лучших практик, которые улучшат ваши Спекуляции разнообразны, не растягивая свои навыки прямо сейчас. Это также небольшое окно в более продвинутые концепции, которые вам нужно будет со временем освоить, чтобы «освоить» тестирование. Я чувствую, что не стоит слишком сильно беспокоить вас с самого начала, потому что это может показаться вам запутанным и запутанным, прежде чем вы выработаете общую картину, которая аккуратно связывает все воедино.

Начнем со скорости. Быстрый набор — это не случайность; это вопрос «постоянного» обслуживания. Прослушивание ваших тестов очень часто очень важно — по крайней мере, если вы на борту с TDD и некоторое время пили Kool-Aid — а наборы быстрых тестов делают гораздо разумнее обратить внимание на то, на что ориентируются тесты вы.

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

Поначалу это может показаться не таким уж плохим, но это не тривиальная проблема. Одним из основных преимуществ набора тестов является то, что он определяет структуру вашего приложения — для меня это, вероятно, самая большая победа TDD. Длительные тестовые прогоны делают эту часть практически невозможной, потому что очень вероятно, что вы не запустите их, чтобы не нарушить ваш поток. Быстрые тесты гарантируют, что у вас нет причин не запускать тесты.

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

Также имейте в виду, что вы уже научились разбивать свои тесты и что вам не нужно постоянно запускать полный набор тестов. Вы можете легко запускать отдельные файлы или даже отдельные блоки — и все это в редакторе кода, способном работать с ним, даже не оставляя его для терминала. Например, вы можете сфокусировать тест на тестируемой линии. Если честно, это похоже на магию — никогда не бывает скучно.

Чрезмерная запись в базу данных — часто очень ненужная — это один из надежных способов быстрого замедления набора тестов. Во многих тестовых сценариях вы можете подделать данные, необходимые для настройки теста, и просто сосредоточиться на данных, которые непосредственно тестируются. Большую часть времени вам не нужно обращаться к базе данных, особенно если это касается частей, которые не проходят тестирование напрямую и поддерживают его только каким-либо образом: вошедший в систему пользователь, пока вы тестируете сумму, которую платите в Оформить заказ, например. Пользователь похож на лишнего, которого можно обмануть.

Вы должны попытаться избежать попадания в базу данных в максимально возможной степени, потому что это отнимает большой кусок медленного набора тестов. Кроме того, постарайтесь не устанавливать слишком много данных, если они вам вообще не нужны. Это может быть очень легко забыть, особенно с интеграционными тестами. Модульные тесты часто более сфокусированы по определению. Эта стратегия окажется очень эффективной во избежание замедления тестовых наборов с течением времени. Тщательно выбирайте свои зависимости и посмотрите, какой наименьший объем данных проходит ваши тесты.

Я не хочу сейчас вдаваться в подробности — вероятно, слишком рано в вашей траектории говорить о заглушках, шпионах, подделках и прочем. Путать вас здесь с такими продвинутыми концепциями кажется контрпродуктивным, и вы скоро столкнетесь с ними. Есть много стратегий для быстрых тестов, которые также включают другие инструменты, кроме RSpec. А пока попробуйте обернуть голову вокруг более широкой картины с помощью RSpec и тестирования в целом.

Вы также хотите стремиться проверить все только один раз — если это возможно. Не проверяйте одно и то же снова и снова — это просто расточительно. В основном это происходит случайно и / или из-за плохих дизайнерских решений Если у вас начались медленные тесты, это простое место для рефакторинга, чтобы получить ускорение.

Большинство ваших тестов также должны проводиться на уровне подразделения, тестируя ваши модели. Это не только ускорит процесс, но и обеспечит максимальную отдачу. Интеграционные тесты, которые тестируют целые рабочие процессы — имитируя поведение пользователя до некоторой степени путем объединения нескольких компонентов и синхронного тестирования их — должны быть самой маленькой частью вашей тестовой пирамиды. Это довольно медленно и «дорого». Может быть, 10% ваших общих тестов не является нереальным, но это зависит, я думаю.

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

Сервер Spring является функцией Rails и предварительно загружает ваше приложение. Это еще одна прямолинейная стратегия, позволяющая значительно увеличить скорость тестирования — сразу после установки, я должен добавить. Он просто поддерживает работу приложения в фоновом режиме, не загружая его при каждом тесте. То же самое относится к задачам Rake и миграциям; они также будут работать быстрее.

Начиная с Rails 4.1, Spring был включен в Rails — автоматически добавлен в Gemfile — и вам не нужно много делать, чтобы запустить или остановить этот предварительный загрузчик. В прошлом нам приходилось подключать для этого наши собственные инструменты выбора, что вы, конечно, можете сделать, если у вас есть другие предпочтения. Что действительно приятно и продуманно, так это то, что он автоматически перезапустится, если вы измените некоторые гемы, инициализаторы или конфигурационные файлы — приятное и удобное прикосновение, потому что легко забыть позаботиться об этом самостоятельно.

По умолчанию он настроен на запуск только rails и rake . Поэтому нам нужно настроить его на запуск с rspec команды rspec для запуска наших тестов. Вы можете запросить статус весны следующим образом:

1
spring status
1
Spring is not running.

Поскольку выходные данные говорят нам, что Spring не работает, вы просто запускаете его с spring server. Spring spring server. Когда вы сейчас запускаете spring status , вы должны увидеть что-то похожее на это:

1
2
3
Spring is running:
 
3738 spring server |

Теперь мы должны проверить, какой Spring настроен для предварительной загрузки.

1
spring binstub —all
1
2
3
* bin/rake: spring already present
 
* bin/rails: spring already present

Это говорит нам о том, что Spring предварительно загружает Rails для команд rake и rails , и пока ничего больше. Об этом нам нужно позаботиться. Нам нужно добавить gem spring-commands-rspec , и наши тесты будут готовы к предварительной загрузке.

1
gem ‘spring-commands-rspec’, group: :development
1
2
3
bundle install
 
bundle exec spring binstub rspec

Я избавляю вас от результатов bundle install ; Я уверен, что вы уже видели больше, чем ваша справедливая доля. С другой стороны, при bin/rspec bundle exec spring binstub rspec генерируется файл bin/rspec который в основном добавляет его для предварительной bin/rspec Spring. Посмотрим, сработало ли это:

1
spring binstub —all

Это создало нечто, называемое binstub — оболочка для таких исполняемых файлов, как rails , rake, bundle , rspec и т. Д., Так что при использовании команды rspec будет использоваться Spring. Кроме того, такие binstubs гарантируют, что вы запускаете эти исполняемые файлы в правильной среде. Они также позволяют вам запускать эти команды из любого каталога в вашем приложении, а не только из корня. Другим преимуществом binstubs является то, что вам не нужно добавлять bundle exec ко всему.

1
2
3
4
5
* bin/rake: spring already present
 
* bin/rspec: spring already present
 
* bin/rails: spring already present

Выглядит хорошо! Давайте остановим и перезапустим сервер Spring, прежде чем двигаться дальше:

1
2
3
spring stop
 
spring server

Итак, теперь вы запускаете сервер Spring в одном выделенном окне терминала и запускаете свои тесты с немного другим синтаксисом в другом. Нам просто нужно ставить перед каждым тестом префикс командой spring :

1
spring rspec spec

Конечно, он запускает все ваши спецификации файлов. Но нет необходимости останавливаться на достигнутом. Вы также можете запускать отдельные файлы или тесты с тегами через Spring — без проблем! И все они будут молниеносно сейчас; на небольших тестовых наборах они действительно кажутся почти мгновенными. Кроме того, вы можете использовать один и тот же синтаксис для команд rails и rake . Хорошо, а?

1
2
3
4
5
6
7
spring rake
 
spring rails g model BondGirl name:string
 
spring rake db:migrate
 

Итак, мы получили Spring из коробки, чтобы ускорить процесс в Rails, но мы не должны забывать добавить этот маленький Gem, чтобы Spring знал, как играть в мяч с RSpec.

Вещи, упомянутые в этом разделе, вероятно, стоит избегать, если вы можете найти другое решение для них. Чрезмерное использование некоторых удобств RSpec может привести к развитию плохих привычек тестирования — по крайней мере, сомнительных. То, что мы обсудим здесь, удобно на поверхности, но может укусить вас чуть позже.

Их не следует рассматривать как AntiPatterns — вещи, которых следует избегать сразу, — скорее их следует рассматривать как «запахи», вещи, к которым вы должны быть осторожны и которые могут привести к значительным затратам, которые вы часто не хотите платить. Обоснование этого включает в себя еще несколько идей и концепций, с которыми вы, как новичок, скорее всего, еще не знакомы — и, откровенно говоря, на данный момент, возможно, вам это покажется немного — но я должен, по крайней мере, отправить вас домой с несколькими красные флаги, чтобы думать и зафиксировать в памяти на данный момент.

  • let

На первый взгляд наличие большого количества ссылок let может показаться очень удобным, особенно потому, что они сильно сушат. Поначалу кажется достаточно неплохим извлечением, чтобы они были в верхней части ваших файлов, например. С другой стороны, они могут легко затруднить понимание вашего собственного кода, если вы будете посещать определенные тесты через некоторое значительное время. Отсутствие данных, установленных в ваших блоках let не слишком помогает пониманию ваших тестов. Это не так тривиально, как может показаться на первый взгляд, особенно если в дело вовлечены другие разработчики, которым нужно читать и вашу работу.

Такая путаница становится намного дороже, чем больше разработчиков. Это не только отнимает много времени, если вам приходится выискивать ссылки снова и снова, это также глупо, потому что этого можно было бы избежать без особых усилий. Ясность — король, без сомнения об этом. Еще один аргумент в пользу сохранения этих данных в том, что ваш набор тестов будет менее хрупким. Вы не хотите строить карточный домик, который становится все более нестабильным с каждым let который скрывает детали от каждого теста. Вы, наверное, узнали, что использование глобальных переменных не очень хорошая идея. В этом смысле let полуглобальный в ваших спецификациях.

Другая проблема заключается в том, что вам нужно будет протестировать множество разных вариантов, разных состояний для похожих сценариев. Вскоре у вас закончатся операторы let разумными именами, чтобы охватить все различные версии, которые вам могут понадобиться, или в итоге вы получите стог сена в тоннах вариаций состояний с одинаковыми именами. Когда вы настраиваете данные в каждом тесте напрямую, у вас нет этой проблемы. Локальные переменные дешевы, хорошо читаемы и не мешают другим областям. На самом деле, они могут быть еще более выразительными, потому что вам не нужно учитывать множество других тестов, которые могут иметь проблемы с определенным именем. Вы хотите избежать создания еще одного DSL поверх фреймворка, который каждый должен расшифровать для каждого теста, использующего let . Я надеюсь, что это похоже на пустую трата времени каждого.

  • before и after

Сохраняйте такие вещи, как before , after и их варианты для особых случаев, и не используйте их постоянно, везде. Посмотрите на это как на одно из больших орудий, которые вы достали для мета. Очистка ваших данных — хороший пример, который слишком мета для каждого отдельного теста. Вы хотите извлечь это, конечно.

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

В конце концов, вы чувствуете, что слишком много веревки, чтобы повеситься, потому что let представляет широко распространенные светильники. Это в основном сводится к фиктивным тестовым данным, объем которых недостаточно узок.

Это легко приводит к одному главному запаху, называемому «тайный гость». Это означает, что у вас есть тестовые данные, которые появляются из ниоткуда или просто предполагаются. Вам часто нужно сначала выследить их, чтобы понять тест, особенно если прошло некоторое время с тех пор, как вы написали код, или если вы новичок в базе кода. Гораздо эффективнее определять свои тестовые данные в точности там, где они вам нужны — при настройке конкретного теста, а не в гораздо более широкой области.

01
02
03
04
05
06
07
08
09
10
11
 
 
 
describe Agent, ‘#print_favorite_gadget’ do
  it ‘prints out the agents name, rank and favorite gadget’ do
    expect(agent.print_favorite_gadget).to eq(‘Commander Bond has a thing for Aston Martins’)
  end
end

Когда вы смотрите на это, оно читается довольно хорошо, верно? Это сжато, однострочно, довольно чисто, нет? Давайте не будем обманывать себя. Этот тест не говорит нам много о рассматриваемом agent , и он не рассказывает нам всю историю. Детали реализации важны, но мы не видим ничего из этого. Агент, кажется, был создан где-то в другом месте, и мы должны были бы сначала выследить его, чтобы полностью понять, что здесь происходит. Так что это может выглядеть элегантно на первый взгляд, но это стоит дорого.

Да, ваши тесты могут в конечном итоге не быть СУХОЙ в этом отношении, но это небольшая цена за то, чтобы быть более выразительной и более легкой для понимания, я думаю. Конечно, есть исключения, но они действительно должны быть применены к исключительным обстоятельствам после того, как вы сразу исчерпали опции, которые предлагает чистый Ruby.

С загадочным гостем вы должны выяснить, откуда берутся данные, почему они важны и каковы их особенности. Отсутствие деталей реализации в конкретном тесте только делает вашу жизнь сложнее, чем нужно. Я имею в виду, делайте то, что вы чувствуете, если вы работаете над своими собственными проектами, но когда другие разработчики участвуют, было бы лучше подумать о том, чтобы сделать их опыт работы с вашим кодом как можно более плавным.

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

Короче говоря, вы хотите иметь тесты, которые легко читать и которые легче рассуждать — на основе каждого теста. Попытайтесь указать все, что вам нужно в реальном тесте — и не более того. Этот вид отходов начинает «пахнуть», как и любой другой вид мусора. Это также подразумевает, что вы должны добавлять детали, необходимые для конкретных тестов, как можно позже — когда вы создаете тестовые данные в целом, в рамках реального сценария, а не в каком-то удаленном месте. Предлагаемое использование let предлагает другой вид удобства, который, кажется, противостоит этой идее.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
 
 
 
describe Agent, ‘#print_favorite_gadget’ do
  it ‘prints out the agents name, rank and favorite gadget’ do
    agent = Agent.new(name: ‘James Bond’, rank: ‘Commander’, favorite_gadget: ‘Aston Martin’)
 
    expect(agent.print_favorite_gadget).to eq(‘Commander Bond has a thing for Aston Martins’)
  end
end

Было бы неплохо, если бы вы позволили вам настроить базовые тестовые данные, которые вы могли бы усовершенствовать по мере необходимости в каждом конкретном тесте, но это не то, как let катится. Именно так мы используем фабрики через Factory Girl .

Я избавлю вас от подробностей, тем более что я уже написал несколько статей об этом. Вот мои статьи, посвященные новичкам 101 и 201, о том, что может предложить Factory Girl, если вам это уже интересно. Он также написан для разработчиков без большого опыта.

Давайте рассмотрим еще один простой пример, который хорошо использует поддержку тестовых данных, которые встроены:

01
02
03
04
05
06
07
08
09
10
describe Agent, ‘#current_mission’ do
 
  it ‘prints out the agent’s current mission status and its objective’ do
    mission_octopussy = Mission.new(name: ‘Octopussy’, objective: ‘Stop bad white dude’)
    bond = Agent.new(name: ‘James Bond’, status: ‘Undercover operation’, section: ’00’, licence_to_kill: true)
    bond.missions << mission_octopussy
 
    expect(bond.current_mission).to eq (‘Agent Bond is currently engaged in an undercover operation for mission Octopussy which aims to stop bad white dude’)
  end
end

Как видите, у нас есть вся информация, необходимая для этого теста, в одном месте, и нам не нужно выискивать какие-либо особенности в другом месте. Он рассказывает историю и не скрыт. Как уже упоминалось, это не лучшая стратегия для СУХОГО кода. Хотя это хорошо. Ясность и удобочитаемость перевешивают этот небольшой повторяющийся код, особенно в больших кодовых базах.

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

Как вы думаете, будете ли вы счастливы, если вам нужно сначала расшифровать компоненты установки, чтобы понять и исправить этот провальный тест, прежде чем вы сможете продолжить работу с совершенно другой функцией, над которой вы работаете? Думаю, нет! Вы хотите как можно скорее выйти из этой «несвязанной» спецификации и вернуться к завершению другой функции.

Когда вы найдете все тестовые данные прямо там, где ваши тесты сообщают вам, где они терпят неудачу, вы увеличиваете свои шансы на то, чтобы исправить это быстро, не «загружая» совершенно другую часть приложения в ваш мозг.

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

Допустим, вы нашли пару повторяющихся приспособлений, которые начинают казаться немного грязными. Вместо того, чтобы использовать let или subject , определите метод в нижней части блока описания — соглашение — и извлеките в него общие черты. Если он используется немного шире в файле, вы также можете поместить его внизу файла.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
describe Agent, ‘#current_status’ do
 
  it ‘speculates about the agent’s choice of destination if status is vacation’ do
    bond = Agent.new(name: ‘James Bond’, status: ‘On vacation’, section: ’00’, licence_to_kill: true)
 
    expect(bond.current_status).to eq (‘Commander Bond is on vacation, probably in the Bahamas’)
  end
 
  it ‘speculates about the quartermaster’s choice of destination if status is vacation’ do
    q = Agent.new(name: ‘Q’, status: ‘On vacation’, section: ’00’, licence_to_kill: true)
 
    expect(q.current_status).to eq (‘The quartermaster is on vacation, probably at DEF CON’)
  end
end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
describe Agent, ‘#current_status’ do
 
  it ‘speculates about the agent’s choice of destination if status is vacation’ do
    bond = build_agent_on_vacation(‘James Bond’, ‘On vacation’)
 
    expect(bond.current_status).to eq (‘Commander Bond is on vacation, probably in the Bahamas’)
  end
 
  it ‘speculates about the quartermaster’s choice of destination if status is vacation’ do
    q = build_agent_on_vacation(‘Q’, ‘On Vacation’)
 
    expect(q.current_status).to eq (‘The quartermaster is on vacation, probably at DEF CON’)
  end
 
  def build_agent_on_vacation(name, status)
    Agent.new(name: name, status: status, section: ’00’, licence_to_kill: true)
  end
end

Теперь наш извлеченный метод заботится о section и licence_to_kill и, таким образом, не отвлекает нас от licence_to_kill теста. Конечно, это фиктивный пример, но вы можете масштабировать его сложность столько, сколько вам нужно. Стратегия не меняется. Это очень простая техника рефакторинга, поэтому я и представляю ее так рано, но одна из самых эффективных. Кроме того, это позволяет без труда избегать инструментов извлечения, которые предлагает RSpecs.

Также следует обратить внимание на то, насколько выразительными могут быть эти вспомогательные методы без дополнительной платы.

Избегание нескольких частей RSpec DSL и хорошее использование хороших принципов Ruby и объектно-ориентированного программирования — это хороший способ приблизиться к написанию ваших тестов. Вы можете свободно использовать основы, describe , context и it , конечно.

Найдите вескую причину для использования других частей RSpec и избегайте их как можно дольше. Просто потому, что вещи могут казаться удобными и причудливыми, не является достаточной причиной для их использования — лучше держать вещи проще.

Простое это хорошо; он сохраняет ваши тесты здоровыми и быстрыми.