Статьи

Максимум вашего TDD с Maxitest и Minitest

Уровень стресса

Цель, стоящая за мини-тестом, может быть выражена одним словом: просто . Minitest убирается с вашего пути и позволяет вам писать выразительные и удобочитаемые тесты. Эти идеалы велики, но иногда мы хотим, чтобы на наших испытаниях было немного магии. это то, что доставляет Maxitest . Maxitest добавляет такие функции, как context блоки и выполнение по номеру строки, которые делают ваш набор тестов немного лучше. Но это еще не все. Давайте погрузимся в!

Хотя я упомяну maxitest в качестве нашего «набора тестов», важно помнить, что minitest — это реальная сила нашего тестирования. Maxitest — это просто набор дополнений к minitest, которые облегчают жизнь.

Приложение

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

Давайте rails new действие:

 $ rails new news_feed # such a creative title, I know # ... $ cd news_feed 

Теперь добавьте maxitest и minitest-spec-rails в ваш Gemfile .

 group :development, :test do # ... gem 'maxitest' gem 'minitest-spec-rails' end 

Драгоценный камень minitest-spec-rails Minitest::Spec интеграцию Minitest::Spec с Rails, расширяя Minitest::Spec в собственные классы тестов Rails. Вы можете прочитать больше на readme .

Сначала давайте взглянем на наш test_helper.rb и добавим require для maxitest. Если вы добавляете это в существующую кодовую базу, вам просто нужно изменить mini на maxi :

 ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'maxitest/autorun' # requires maxitest for test running require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end 

Теперь давайте сделаем что-то для тестирования. Для начала просто создайте модель пользователя с двумя атрибутами: first_name и last_name :

 $ rails g model user first_name:string last_name:string 

Используя TDD, мы собираемся объединить эти два атрибута в полное имя. Так что, будучи догматичным тестом-первыми людьми, которыми мы являемся (или, по крайней мере, хотелось бы), давайте напишем тест!

Тестовый файл уже должен быть сгенерирован в test / models / user_test.rb .

 # test/models/user_test.rb class UserTest < ActiveSupport::TestCase describe '#name' do # our method it 'should concatenate first and last names' do user = User.new(first_name: 'Johnny', last_name: 'Appleseed') assert_equal user.name, "#{user.first_name} #{user.last_name}" end end end 

Класс UserTest унаследован от ActiveSupport::TestCase , в котором уже есть Minitest::Spec::DSL расширенный гемом minitest-spec-rails. Затем мы используем describe для описания того, что мы тестируем. Мы используем блок it для описания поведения этого метода. Внутри тест создает экземпляр User а затем гарантирует, что #name действительно возвращает объединение first_name и last_name .

Давайте запустим тестовый набор:

 $ rake test 

Тест-люкс-красный-ошибка

Ура! Красный. Теперь давайте создадим метод для получения зеленого цвета .

 # app/models/user.rb class User < ActiveRecord::Base def name "#{first_name} #{last_name}" end end 

Чтобы запустить тест в этот раз, мы скопируем и вставим фрагмент кода, который нам дал maxitest, чтобы снова запустить этот тест (построчно).

 # the -I option tells mtest to include the 'test' directory mtest test/models/user_test.rb:5 -I test 

тест-люкс-первых, зеленый

Большой! Как видите, maxitest добавляет удовлетворительный зеленый цвет к успешным тестам. Эта особенность учитывает действительно красный — зеленый — процесс рефакторинга . Говоря об этом, давайте немного реорганизовать наш код. Я решил, что интерполяция строк может быть не лучшим способом написания нашего кода (это гипотетически; у меня нет предпочтений), поэтому я собираюсь использовать #join для двух строк:

 # app/models/user.rb class User < ActiveRecord::Base def name [first_name, last_name].join(' ') end end 

Давайте запустим наши тесты, чтобы убедиться, что мы ничего не сломали … ( $ rake test ) Мы этого не сделали.

let_all

Теперь мы собираемся немного реорганизовать наши тесты и показать метод let_all let_all . Чтобы понять let_all , важно понять, что делает сам себя. Создайте метод доступа, назначенный блоку, который вы передаете. Это ничем не отличается от определения метода внутри блока describe . let_all — это расширение функциональности let , кэширующее ответ переданного блока. По сути, это создает кешированную глобальную переменную для вашего блока.

Обратите внимание, что в user_test.rb мы настраиваем блок it с помощью назначения переменной. Мы можем преобразовать это в блок let_all внутри блока describe #name . Это практично, потому что все блоки внутри блока describe #name могут полагаться на одни и те же данные настройки и, следовательно, работать быстрее.

Наш новый, user_test.rb :

 require 'test_helper' class UserTest < ActiveSupport::TestCase describe '#name' do # our method let_all(:user) { User.new(first_name: 'Johnny', last_name: 'Appleseed') } it 'should concatenate first and last names' do assert_equal user.name, "#{user.first_name} #{user.last_name}" end end end 

позволять!

С другой стороны спектра maxitest добавляет let! метод. let! гарантирует, что переданный ему блок всегда оценивается при каждом использовании метода.

Неявные субъекты: никогда, никогда не используйте их.

Помимо текущего кода, давайте рассмотрим одну из наиболее эксцентричных функций maxitest: неявные субъекты. Я настоятельно рекомендую не использовать это. Когда-либо. «Неявный субъект» — это синоним гостя Mystery , что делает код гораздо менее понятным и полезным. Хотя неявный предмет может быть полезен для вас прямо сейчас, если вы сэкономите несколько секунд, когда вы или другой разработчик добавите в базу кода функцию пробития тестов через пять месяцев, и вы не сможете выяснить, что тест проверяет, извините

Из-за этого я не могу с чистой совестью продемонстрировать неявную субъектную особенность maxitest. Сам автор не включал неявные темы по умолчанию. Они по своей природе чрезвычайно хакерские.

Блоки контекста

Одна вещь, которую я действительно хочу, чтобы у minitest были блоки context . Функционально они ничем не отличаются от describe блоков, но семантически они есть. Иногда имеет смысл написать часть теста (особенно приемочных) с использованием context .

Maxitest воплотил мое желание в одну строчку:

 # https://github.com/grosser/maxitest/blob/master/lib/maxitest/autorun.rb#L23 # ... Minitest::Spec::DSL.send(:alias_method, :context, :describe) # ... 

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

 $ mkdir test/acceptance $ touch test/acceptance/main_feed_test.rb 

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

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

Чтобы построить это право, нам понадобится модель Post , принадлежащая пользователю. Давайте сгенерируем это:

 $ rails g model Post user_id:integer title:string body:text 

Пока мы на этом, давайте добавим некоторые проверки и ассоциации к нашим сообщениям и пользователям:

 # app/models/post.rb class Post < ActiveRecord::Base validates :title, :body, presence: true belongs_to :user end # app/models/user.rb class User < ActiveRecord::Base validates :first_name, :last_name, presence: true has_many :posts # ... end 

Теперь нам нужно сделать некоторые данные для наших пользователей и постов. Rails автоматически генерирует образцы приборов в тесте / приспособлении , но нам нужно немного их настроить. Вот некоторые данные о приборах, которые я составил, смело меняйте их на что угодно:

 # test/fixtures/users.yml john: first_name: John last_name: Appleseed jane: first_name: Jane last_name: Doe # test/fixtures/posts.yml one: user: john title: A Taste of Latin Filler Text body: Aenean pretium consectetur ex, at facilisis nisl tempor a. Vivamus feugiat sagittis felis. Quisque interdum, risus vel interdum mattis, ante neque vestibulum turpis, iaculis imperdiet neque mi at nisl. Vivamus mollis sit amet ligula eget faucibus. Vestibulum luctus risus nisi, et congue ante consectetur a. Quisque mattis accumsan efficitur. Curabitur porta dolor in nisi consectetur rhoncus. In quis libero scelerisque, sagittis neque at, porta tellus. Aenean metus est, tincidunt sed pellentesque id, aliquet a libero. Sed eget sodales nunc. Fusce lacinia congue felis at placerat. two: user: jane title: Something Catchy body: I have nothing to say! three: user: jane title: A Very Short Post body: This is witty and short. 

Эти YAML-файлы будут генерировать фикстуры для наших тестов, доступные через model_plural_name(:fixture_name) . Теперь, когда у нас есть приборы, давайте настроим нашу среду приемочного тестирования. Rails не поставляется с классом тестового примера для приемочного тестирования, поэтому мы должны сделать его самостоятельно:

 # test/test_helper.rb ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'maxitest/autorun' require 'rails/test_help' require 'capybara/rails' class ActiveSupport::TestCase fixtures :all end class AcceptanceCase < ActiveSupport::TestCase include Rails.application.routes.url_helpers include Rack::Test::Methods include Capybara::DSL def app Rails.application end end 

Мы добавили Capybara в комплект, поэтому обязательно добавьте gem 'capybara' в ваш Gemfile.

Созданный нами класс ( AcceptanceCase ) предоставляет нам приборы, помощники по маршруту, помощники Rack-Test и Capybara.

Теперь давайте напишем тест, чтобы начать разработку функции фида.

 require 'test_helper' class FeedTest < AcceptanceCase it 'should exist' do get root_path assert last_response.ok?, 'GET "/" does not respond OK' end end 

Давайте теперь запустим пакет и посмотрим, что произойдет:

 1) Error: FeedTest#test_0001_should exist: ActionController::RoutingError: No route matches [GET] "/" test/acceptance/feed_test.rb:5:in `block in <class:FeedTest>' 

Похоже, у нас нет маршрута. Давайте сделаем один. Чтобы получить маршрут, нам нужен контроллер, поэтому давайте сгенерируем контроллер #index действием #index :

 $ rails g controller posts index 

Этот генератор также выполняет проверку контроллера сообщений. Также добавим тест для маршрута:

 # test/controllers/posts_controller_test.rb require 'test_helper' class PostsControllerTest < ActionController::TestCase describe '#index' do it 'GET /' do response = get :index assert response.ok? end end end 

Давайте снова запустим пакет, чтобы увидеть новые ошибки:

  1) Error: FeedTest#test_0001_should exist: ActionController::RoutingError: No route matches [GET] "/" test/acceptance/feed_test.rb:5:in `block in <class:FeedTest>' 2) Error: PostsControllerTest::#index#test_0001_GET /: ActionController::UrlGenerationError: No route matches {:action=>"index", :controller=>"posts"} test/controllers/posts_controller_test.rb:6:in `block (2 levels) in <class:PostsControllerTest>' 

Мы видели первую ошибку раньше, но вторая нова; это из нашего поста контроллера контроллера.

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

 # config/routes.rb Rails.application.routes.draw do root to: 'posts#index' end # app/controllers/posts_controller.rb class PostsController < ApplicationController def index end end <!-- app/views/posts/index.html.erb --> <h1>A Listing of Posts!</h1> 

Через 4 строки кода у нас есть 2 новых прохождения теста! Очевидно, что сейчас у нас нет ничего, кроме рабочего маршрута и статической страницы, но это дает нам стабильную основу для построения. Теперь мы можем построить реальную функциональность. Давайте вернемся к feed_test.rb .

Здесь используйте блок context чтобы лучше описать новую функцию в разных контекстах . Для наших целей существует два контекста: с постами и без.

 require 'test_helper' class FeedTest < AcceptanceCase it 'should exist' do get '/' assert last_response.ok?, 'GET "/" does not respond OK' end context 'with posts' do # posts are created before the test runs and can be found in the fixtures # # post #1 -> posts(:one) # post #2 -> posts(:two) # post #3 -> posts(:three) before do visit '/' end let_all(:posts) { page.all('.post') } it 'should list all posts' do assert_equal Post.count, posts.count end it 'should list all post titles' do assert_each_post_has 'title' end it 'should list all post bodies' do assert_each_post_has 'body' end it 'should list all post authors' do assert_each_post_has 'user.name' end end context 'without posts' do before do Post.delete_all visit '/' end it 'should say "There are no posts to be found"' do assert page.has_content? 'There are no posts to be found' end end private def assert_each_post_has(attribute) Post.all.each do |post| # The second argument is a message to display if the assertion fails. # When looping in a test, it's a best practice to ensure that identifying # loop information is returned on failure. assert page.has_content?(post.instance_eval(attribute)), "Post ##{post.id}'s #{attribute} is not displayed" end end end 

Это много кода, но его довольно легко понять; это читает как английский. Давайте просто запустим тесты и посмотрим, достаточно ли они описательны сами по себе без объяснения причин:

 Run options: --seed 44175 # Running: .F..FFFF Finished in 0.257127s, 31.1131 runs/s, 31.1131 assertions/s. 1) Failure: FeedTest::post feed::without posts#test_0001_should say "There are no posts to be found" [/Users/jesse/code/examples/news_feed/test/acceptance/feed_test.rb:47]: Failed assertion, no message given. 2) Failure: FeedTest::post feed::with posts#test_0004_should list all post authors [/Users/jesse/code/examples/news_feed/test/acceptance/feed_test.rb:35]: Post #113629430's user.name is not displayed 3) Failure: FeedTest::post feed::with posts#test_0003_should list all post bodies [/Users/jesse/code/examples/news_feed/test/acceptance/feed_test.rb:31]: Post #113629430's body is not displayed 4) Failure: FeedTest::post feed::with posts#test_0001_should list all posts [/Users/jesse/code/examples/news_feed/test/acceptance/feed_test.rb:23]: Expected: 3 Actual: 0 5) Failure: FeedTest::post feed::with posts#test_0002_should list all post titles [/Users/jesse/code/examples/news_feed/test/acceptance/feed_test.rb:27]: Post #113629430's title is not displayed 8 runs, 8 assertions, 5 failures, 0 errors, 0 skips Focus on failing tests: mtest test/acceptance/feed_test.rb:46 mtest test/acceptance/feed_test.rb:34 mtest test/acceptance/feed_test.rb:30 mtest test/acceptance/feed_test.rb:22 mtest test/acceptance/feed_test.rb:26 

Давайте посмотрим на первый сбой: сразу же я знаю, что тело сообщения (если быть точным # 113629430) не отображается в ленте новостей. Давайте исправим это тогда! Я вижу этот список как руководство по функциям. Это дает мне дополнительные шаги для завершения функции; это здорово! Сначала мы исправим только первый тест, а потом исправим остальные.

 # app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.all end end <% # app/views/posts/index.html.erb %> <h1>A Listing of Posts!</h1> <ul> <% @posts.each do |post| %> <li><%= post.title %></li> <% end %> </ul> 

Это оно! Теперь давайте попробуем mtest test/acceptance/feed_test.rb:26 -I test тест… ( mtest test/acceptance/feed_test.rb:26 -I test ) Успех! Это было довольно легко, давайте исправим все остальное. Тестирование отлично подходит для создания дополнительных функций. Чтобы исправить следующие тесты, все, что нам нужно сделать, это посмотреть на одну ошибку, исправить ее, а затем исправить следующую.

Вот конечный продукт!

 <% # app/views/posts/index.html.erb %> <h1>A Listing of Posts!</h1> <% if @posts.empty? %> <p>There are no posts to be found!</p> <% else %> <ul> <% @posts.each do |post| %> <li class="post"> <%= post.title %> <ul> <li><strong>Author: </strong> <%= post.user.name %></li> <li><em><%= post.body %></em></li> </ul> </li> <% end %> </ul> <% end %> 

Очевидно, что это далеко не полная функция, но мы получаем ядро ​​TDD с большими дополнениями от maxitest.

Вывод

Я просто просмотрел поверхность обширной философии и отличного инструмента. Minitest с maxitest составляют мощную, но в то же время легкую комбинацию для внедрения управляемой тестированием разработки в ваши приложения Ruby. В следующий раз, когда вы начинаете новый проект, подумайте дважды о RSpec и подумайте, как попробовать minitest и maxitest.

Заинтересованы в дополнительных темах тестирования? Я хотел бы услышать их! Не стесняйтесь, чтобы оставить комментарий ниже.