Тестирование, RSpec

Как был бы прекрасен мир вообще без багов. Сейчас мы научим вас секретам успеха волшебной технике снижения количество ошибок в любой программе!

План урока

  1. Что такое тесты и тестирование?
  2. Инструменты написания тестов, RSpec
  3. Пишем тесты для «склонятора»

Что такое тестирование вообще и тестирование для программиста

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

Зарубите на носу :) В любой настоящей программе всегда есть баги. К тому же, программы нужно постоянно улучшать, делать новые версии. Любое изменение программы создает почву для появления новых багов.

Но не волнуйтесь, не все так мрачно. Цель хорошего программиста: во-первых писать программы так, чтобы свести количество багов к минимуму, а во-вторых писать программы так, чтобы они работали несмотря на несущественные баги.

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

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

Зачем нужны тесты?

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

Эта гениальная идея давно пришла в голову программистам и хороший программист всегда пишет еще и тесты. Это убивает сразу кучу зайцев.

Вот лишь некоторые из них:

  • Когда вы пишите тесты, вы начинаете гораздо лучше понимать логику работы программы
  • Хорошие тесты помогают вам спокойно вносить изменения в программу и не бояться, что что-то сломается незаметно для вас
  • Тесты в разы ускоряют проверку работоспособности написанной программы

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

Как делают тесты?

Подходов к тестированию очень много. Но самое главное в тестах не подход, а просто наличие тестов!

Любой тест подразумевает, что даны какие-то условия и программа (или её часть) должны в этих условия выдать конкретный (тоже заранее известный) результат.

Именно поэтому тесты помогают понять задачу: вы не можете написать тест, пока не поняли, что в каких условиях должно получиться.

Библиотека для написать тестов на Ruby — Rspec

Для написания тестов есть множество утилит. И в разных языках они могут быть очень разные. Давайте изучим самую популярную в руби — RSpec.

RSpec - лучший друг ruby-разработчика

gem install rspec

Как устроены тесты

По своей структуре тесты — это отдельные программки, проверяющие ключевые функции вашей программы (или несколько функций вплоть до всей программы целиком).

Они передают программе заданные параметры, а потом смотрят, соответствует ли её ответ заданному в тесте. Если соответствует — хорошо, а нет — тест считается упавшим. И это скорее всего означает, что в программе ошибка.

Тесты атакуют программу

Нагляднее всего протестировать нашу функцию склонения слов из 9 урока базового блока ([lesson_id=9]). Мы там написали метод, который должен выводить нужную форму слова в зависимости от того, какое число ему передали.

1 → крокодил
3 → крокодила
6 → крокодилов

Давайте напишем тесты для метода sklonenie:

Тестируем склонение

Создайте в папке урока папку sklonjator и создайте в ней файл sklonjator.rb, наш метод мы для удобства завернем в класс Sklonjator:

# encoding: utf-8
# метод для склонения русских слов в соответствии с числительным, преобразовано к классу

class Sklonjator

  # Статический метод будет возвращать правильно склонение слова,
  # когда нужно его использовать с числом
  # Например во фразах, типа "1 крокодил, 23 крокодила, 7 крокодилов"
  def self.sklonenie(number, krokodil, krokodila, krokodilov)
    # проверим входные данные на правильность
    if (number == nil || !number.is_a?(Numeric))
      number = 0 # если первый параметр пустой или не число, то продолжаем как будто он нулевой
    end

    ostatok = number % 10 # склонение определяется последней цифрой в числе

    if (ostatok == 1) # для 1 - именительный падеж (Кто? Что?)
      return krokodil
    end

    if (ostatok >= 2 && ostatok <= 4) # для 2-4 - родительный падеж (Кого? Чего?)
      return krokodila
    end

    # 5-9 или ноль – родительный падеж и множественное число
    if (ostatok >= 5 && ostatok <= 9 || ostatok == 0)
      return krokodilov
    end
  end

end

И давайте теперь его протестируем, создайте в папке sklonjator файл sklonjator_spec.rb.

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

Все тесты пишутся на языке Ruby, но с дополнительными конструкциям, которые добавляет гем RSpec:

# подключаем сам rspec
require 'rspec'

# подключаем склонятор
require_relative 'sklonjator.rb'

# так в RSpec начинается сценарий для конкретного класса/модуля/метода
describe Sklonjator do

  # внутри идет набор кейсов внутри it '...' do ... end
  # каждый такой кейс выполняется rspec-ом при запуске всего сценария в случайном порядке

  it 'should do ok for KROKODILOV' do
    # ключевое слово-метод expect(...).to ...
    # ожидаем-что( нечто ).to - будет чем-то, например "eq" значит равно
    # обо всех возможностях RSpec см. документацию и материалы к уроку
    expect(Sklonjator.sklonenie(0, 'krokodil', 'krokodila', 'krokodilov')).to eq 'krokodilov'
    expect(Sklonjator.sklonenie(5, 'krokodil', 'krokodila', 'krokodilov')).to eq 'krokodilov'
    expect(Sklonjator.sklonenie(6, 'krokodil', 'krokodila', 'krokodilov')).to eq 'krokodilov'
  end

  # простые случаи для КРОКОДИЛ
  it 'should do ok for KROKODIL ' do
    [1, 21, 31].each do |i|
      expect("#{i} #{Sklonjator.sklonenie(i, 'krokodil', 'krokodila', 'krokodilov')}").to eq "#{i} krokodil"
    end
  end

  # простые случаи для КРОКОДИЛА
  it 'should do ok for KROKODILA ' do
    [2, 3, 4, 22, 33].each do |i|
      expect("#{i} #{Sklonjator.sklonenie(i, 'krokodil', 'krokodila', 'krokodilov')}").to eq "#{i} krokodila"
    end
  end
end

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

cd c:\rubytut2\lesson17\sklonjator
rspec sklonjator_spec.rb

Запуск RSpec из консоли

Или можно запустить тест прямо в RubyMine.

Отчёт RSpec о прошедших тестах

Все наши тесты прошли. Давайте теперь напишем какой-нибудь тест на тот случай, который сейчас в нашей программе не учтён.

  # ОСОБЫЕ случаи
  # этот тест должен упасть, чтобы он заработал — надо починить склонятор (см. исходник склонятора)
  it 'should do ok for KROKODILOV - SPECIAL' do
    [10, 11, 12, 13, 14, 111, 312, 1013, 2414].each do |i|
      expect("#{i} #{Sklonjator.sklonenie(i, 'krokodil', 'krokodila', 'krokodilov')}").to eq "#{i} krokodilov"
    end
  end

Снова запустим тесты:

Отчёт RSpec об упавших тестах

Ну вот, теперь другое дело! Скорее чинить склонятор!

  ostatok100 = number % 100
  if (ostatok100 >= 11 && ostatok100 <= 14)
    return krokodilov
  end

Теперь все тесты должны пройти.

Рефакторинг с тестами

Для того, чтобы продемонстрировать пользу тестов, давайте сделаем улучшение нашей программы.

Раньше мы передавали массив с четырьмя параметрами и ждали только нужную форму слова. А теперь давайте допишем метод так, чтобы он принимал пятый необязательный параметр with_number, который указывает, хотим ли мы увидеть в возвращаемой строке наше число:

Sklonjator.sklonenie(5, 'krokodil', 'krokodila', 'krokodilov', false)
# → krokodilov

Sklonjator.sklonenie(5, 'krokodil', 'krokodila', 'krokodilov', true)
# → 5 krokodilov

Сперва напишем соответствующие тесты:

  # вывод числа вместе с формой слова
  it 'should print number if with_numbers = true' do
    expect(Sklonjator.sklonenie(5, 'krokodil', 'krokodila', 'krokodilov', true)).to eq "5 krokodilov"
  end

  it 'should not print number if with_numbers = false' do
    expect(Sklonjator.sklonenie(5, 'krokodil', 'krokodila', 'krokodilov', false)).to eq "krokodilov"
  end

После того, как мы написали тесты, можно писать наши улучшения в программе:

# encoding: utf-8
# метод для склонения русских слов в соответствии с числительным, преобразовано к классу

# ОТРЕФАКТОРЕННАЯ версия — с другим методом, с дополнительной опцией вывода строки вместе с числом

class Sklonjator
  def self.sklonenie(number, krokodil, krokodila, krokodilov, with_number)
    # проверим входные данные на правильность
    if (number == nil || !number.is_a?(Numeric))
      number = 0 # если первый параметр пустой или не число, то продолжаем как будто он нулевой
    end

    # определяем выводить ли число перед крокодилами, в зависимости от опции
    prefix = ""
    prefix = "#{number.to_s} " if with_number

    ostatok = number % 10 # склонение определяется последней цифрой в числе

    ostatok100 = number % 100
    if (ostatok100 >= 11 && ostatok100 <= 14)
      return "#{prefix}#{krokodilov}"
    end

    if (ostatok == 1) # для 1 - именительный падеж (Кто? Что?)
      return "#{prefix}#{krokodil}"
    end

    if (ostatok >= 2 && ostatok <= 4) # для 2-4 - родительный падеж (Кого? Чего?)
      return "#{prefix}#{krokodila}"
    end

    # 5-9 или ноль – родительный падеж и множественное число
    if (ostatok >= 5 && ostatok <= 9 || ostatok == 0)
      return "#{prefix}#{krokodilov}"
    end
  end
end

Если запустить тесты сейчас, то мы увидим, что те тесты, где мы указали последний параметр (true или false), прошли, а вот все предыдущие упали. Потому что в них мы вызываем метод sklonenie с недостаточным количеством параметров.

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

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

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

def self.sklonenie(number, krokodil, krokodila, krokodilov, with_number = false)

Теперь все тесты должны пройти.

Еще раз повторим, зачем нужны тесты

  • Лучше понимание задачи
  • Благодаря этому лучше и понятнее код
  • Надёжность — после рефакторинга вы увидите, если что-то сломалось
  • Уверенность в завтрашнем дне (вы больше не боитесь трогать ваш код: «Работает — не трогай»)

Когда, что и как тестировать?

У тестирования есть и обратная сторона, из-за непонимания которой, многие разработчики от него отказываются.

Если вы пишете тесты, вы пишете дополнительный код. Который непосредственно не решает ваши задачи, а лишь помогает качественно написать тот, который решает.

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

Это признак того, что вы во-первых зазнались :), а во-вторых — скорее всего пишите тесты не оптимально.

Тесты — это хорошо, а всё хорошее хорошо в меру. Чувству меры нельзя научиться за один день, это будет приходить постепенно с опытом.

А пока несколько советов, когда стоит писать тесты, а когда не обязательно.

Когда нужно писать тесты

  • Критичные участки кода (от которых зависит основная логика программы)
  • Нетривиальные, сложные методы и классы (баги как мухи на навоз слетаются на сложный код)
  • Если вы нашли и исправили баг — напишите тест, который этот баг воспроизводит и проверяет, что теперь он исправлен
  • Граничные условия (например, слишком большие и слишком маленькие числа) — в тестах стоит проверять данные, которые для вашей программы являются на грани адекватных
  • Если есть сомнения или непонимание какого-то кода, на этот код не помешает написать тест

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

Понимание как писать тесты придёт с опытом — пока начните тестировать хоть что-нибудь, хоть как-нибудь. Потому что на тесты (как на что-то неважное) забивают частенько даже опытные программисты. Не будьте из их числа.