Министерство образования и науки Российской Федерации Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «Волгоградский государственный технический университет» Факультет электроники и вычислительной техники Кафедра «ЭВМ» Семестровая работа Тема: Тестирование Rspec Выполнил: Хо Фук Фыонг Группа Принял: Оценка: Волгоград 2014 Содержание 1. Введение 2. Задача и её решение 2.1. Формулировка задачи 2.2. Обсуждение задачи 2.3. Имя файла 2.4. Заготовка для решения 2.5. Создание метода (функции) 2.6. Вызов функции из другого файла 2.7. Модификация программы 3. Тестирование написанной программы 3.1. Создание заготовки для тестов 3.2. Техническая сторона написания тестов 3.3. Содержательная сторона написания тестов 1. Введение Является очевидным тот факт, что тестировать программы полезно. Ведь даже научившись конструировать теоретически правильные программы можно допустить ошибку в процессе написания кода. Чуть позже мы узнаем, что тесты помогают даже при создании программ, но пока наша цель — познакомиться с техникой тестирования и научиться писать простые тесты к несложным программам. Для тестирования Ruby-программ мы будем использовать RSpec. 2. Задача и её решение 2.1. Формулировка задачи Напишите программу на языке Ruby, которая читает со стандартного ввода три целых числа , и и печатает на стандартный вывод мощность множества решений уравнения . Как решать эту задачу? 2.2. Обсуждение задачи При решении этой задачи важно понимать, что уравнение вовсе не обязательно является квадратным. Например, при оно превращается в соотношение , которое либо справедливо при любом , либо не справедливо ни при каком. 2.3. Имя файла Как назвать файл, в котором будет размещена программа? Не следует недооценивать значимость этого вопроса. Когда у вас в одной директории (папке) окажется десяток файлов с программами, то только «говорящие» имена позволят быстро находить нужную программу. Поскольку в качестве расширения файла с программой на языке Ruby обычно используется rb, то в качестве полного имени можно захотеть выбрать, например, Мощность.rb. Так, однако, поступать не рекомендуется. Не используйте русских букв в именах файлов! Гораздо лучше подобрать правильный английский термин, которым в данном случае будет слово cardinality, так как по-английски «мощность множества» записывается как Cardinality (см. соответствующую статью в Википедии). Следовательно, именем файла с программой будет cardinality.rb. 2.4. Заготовка для решения Начальный вариант файла cardinality.rb print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i if a != 0 # Уравнение является квадратным ... else # Уравнение не является квадратным ... end Вместо многоточий здесь нужно написать код, который будет верно определять и печатать мощность множества решений уравнения .. Каким он должен быть? Подумав немного, можно предложить такой вариант программы: Второй вариант файла cardinality.rb print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i if a != 0 # Уравнение является квадратным d = b*b - 4*a*c if d < 0 puts 0 elsif d > 0 puts 2 else puts 1 end else # Уравнение не является квадратным if b == 0 and c == 0 puts "continuum" else puts 1 end end Запуская эту программу, легко убедиться, что иногда она работает. Однако, гарантировать её правильность, конечно, нельзя. Вполне возможно, что что-то в ней и не так. Тесты весьма полезны для выяснения данного вопроса. 2.5. Создание метода (функции) Перед тем, как писать тесты, необходимо сначала модифицировать саму программу. Во-первых, следует отделить содержательную часть решения от операций ввода данных и вывода полученных результатов. Оформить содержательную часть программы в виде функции (метода) в нашем случае можно так: Файл cardinality.rb def cardinality(a,b,c) if a != 0 # Уравнение является квадратным d = b*b - 4*a*c if d < 0 0 elsif d > 0 2 else 1 end else # Уравнение не является квадратным if b == 0 and c == 0 "continuum" else 1 end end end print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i puts "Мощность множества решений уравнения равна #{cardinality(a,b,c)}." Функция, которую мы назвали как и файл cardinality, принимает три аргумента и возвращает вычисленный ею результат. При запуске этой программы интерпретатор языка Ruby сначала прочитает и обработает определение метода (функции) cardinality, а уже затем начнёт выполнять действия, связанные с вводом коэффициентов уравнения. В процессе выполнения последней строки будет вызвана функция cardinality, а возвращённое ей значение напечатано. Главное преимущество так модифицированной программы — это возможность вызывать функцию cardinality несколько раз и с различными аргументами. 2.6. Вызов функции из другого файла Написанную функцию можно вызвать и из другого файла. Её может вызвать даже программа, написанная другим человеком, работающем на компьютере, который находится на противоположной стороне Земли! Создадим новый (второй) файл, в котором будут присутствовать только ввод коэффициентов, вызов функции из первого файла и вывод результата. Как назвать этот файл? В нём будет очень мало ценной информации (более того, затем он окажется нам и вовсе ненужным), поэтому ему вполне можно дать ничего не значащее имя qq.rb. Команда cp cardinality.rb qq.rb позволяет скопировать содержимое файла cardinality.rb в файл qq.rb, после чего там нужно лишь удалить лишние строки: Файл qq.rb print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i puts "Мощность множества решений уравнения равна #{cardinality(a,b,c)}." Скопировать файл можно, конечно, и без использования утилиты cp. Модуль NERDTree среды MsiuVim позволяет сделать это достаточно удобным способом. Запустив получившуюся программу, мы обнаружим, что она запрашивает три числа, но затем выдаёт ошибку: qq.rb:8:in `<main>': undefined method `cardinality' for main:Object (NoMethodError) Смысл этой диагностики вполне ясен — исполняющая система не сумела найти определение метода (функции) cardinality. Почему? Потому что мы не указали, где его искать. Сделать это можно с помощью директивы require, в которой указывают имя подключаемого файла: Исправленный файл qq.rb require "./cardinality" print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i puts "Мощность множества решений уравнения равна #{cardinality(a,b,c)}." В данном случае файл cardinality.rb находится в той же самой директории (папке), что и запускаемая программа, поэтому его имени предшествует точка — обозначение текущей директории. В общем случае нужно указывать полный путь до подключаемого файла. Для файлов из предопределённых директорий (обычно содержащих Rubyбиблиотеки) путь указывать не требуется, например, require "rspec". Запустим программу ещё раз. Теперь функция cardinality вызывается! Но почему-то после вывода решения программа вновь пытается что-то ввести… 2.7. Модификация программы При попытке вызова написанной в файле cardinality.rb функции cardinality из другого файла (qq.rb) возникает проблема, связанная с двойным вводом данных. От этого легко избавиться, оставив в файле cardinality.rb только определение функции cardinality. Тогда, однако, нельзя будет увидеть работу данной функции без использования другого файла, а это не слишком удобно. Решить возникшую проблему можно следующим образом: Файл cardinality.rb def cardinality(a,b,c) if a != 0 # Уравнение является квадратным d = b*b - 4*a*c if d < 0 0 elsif d > 0 2 else 1 end else # Уравнение не является квадратным if b == 0 and c == 0 "continuum" else 1 end end end if $0 == __FILE__ print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i puts "Мощность множества решений уравнения равна #{cardinality(a,b,c)}." end Код, находящийся между строками if $0 == __FILE__ и end, выполняются тогда и только тогда, когда имя файла, указываемого при запуске программы, совпадает с именем файла, содержащего эти строки. После такой модификации запуск программ, содержащихся в обоих файлах cardinality.rb и qq.rb, будет приводить к корректному результату. 3. Тестирование написанной программы Для работы с RSpec в системе должен быть установлен соответствующий пакет. В компьютерных классах МГИУ он имеется, а на домашний компьютер его можно установить командой gem install rspec. 3.1. Создание заготовки для тестов Тесты пишутся в отдельном файле, имя которого, как правило, получают, добавляя символы _spec к имени файла, содержащего тестируемый код. В нашем случае — cardinality_spec.rb. Одной из первых строк этого файла должна быть директива подключения файла, содержащего тестируемую функцию. Далее описывается (describe) поведение этой функции в различных ситуациях (context "Функция cardinality"). В языке Ruby имена всех классов и модулей обязаны начинаться с большой буквы, а имена функций, методов и переменных — с маленькой; при этом для имён, составленных из нескольких слов, применяется такое соглашение: для функций, методов и переменных склеивающим символом является подчёркивание, а для классов и модулей каждая из компонент сложного имени начинается с большой буквы. В соответствии с указанными фактами заготовку для тестов мы разместим в файле cardinality_spec.rb и она будет иметь следующий вид: Файл cardinality_spec.rb require './cardinality' describe do context 'Функция cardinality' do ... end end Вместо многоточия в этом файле мы должны сформулировать несколько утверждений о функции cardinality, называть которую будем просто it (она), так как о чём именно идёт речь, уже сказано в директиве context. Вот пример простейшего утверждения: require './cardinality' describe do context 'Функция cardinality' do it 'возвращает 2 при a=1,b=1,c=0' do expect(cardinality(1,1,0)).to eq 2 end end end Здесь (русским языком!) говорится, что (цитата!) Функция cardinality возвращает 2 при a=1,b=1,c=0 Для интерпретатора языка Ruby предназначена строка expect(cardinality(1,1,0)).to eq 2 В ней сказано, что при вызове функции cardinality с аргументами 1,1,0 ожидается (expect to) равенство (eq) результата двум (2). Запустить тест можно командой rspec cardinality_spec.rb Среда MsiuVim позволяет запускать RSpec-тесты непосредственно в ней. Нажатие на клавишу F5 при уже нажатой клавиши Ctrl делает это. Результат запуска теста будет примерно таким: $ rspec cardinality_spec.rb . Finished in 0.00079 seconds 1 example, 0 failures Здесь сказано, что рассмотрен 1 пример (example). При этом неудач (failures) не произошло, иначе говоря, реально полученные результаты совпали с ожидаемыми. В этот момент очень полезно посмотреть, что произойдёт в случае неудачи теста. Проще всего исправить директиву expect в файле cardinality_spec.rb, заменив ожидаемый результат 2 на, скажем, 222. Запускать тест можно с ключом (опцией) -c (rspec -c cardinality_spec.rb), что позволит получить выдачу результата в цвете. 3.2. Техническая сторона написания тестов Бо́льшая часть утверждений, используемых в тестах, имеет следующий формат: expect(actual).to op expected Здесь используются такие обозначения: expect to переводится как «ожидается»; actual это реально вычисляемое (актуальное) значение; expected ожидаемое значение, с которым производится сравнение; op какая-то операция сравнения. Существует достаточно большое количество различных операций сравнения, но в данный момент времени недостаточное знание языка Ruby не позволяет нам даже понять многие из них. Пока нам будут полезны следующие операции: проверки на эквивалентность: o o expect(actual).to eq(expected) этот тест успешно проходит, если actual == expected expect(actual).to eql(expected) этот тест проходит, если actual.eql?(expected) Обратите внимание на разницу между операторами == и eql?: $ pry [1] pry(main)> 1==1.0 => true [2] pry(main)> 1.eql? 1 => true [3] pry(main)> 1.eql? 1.0 => false проверки на идентичность: o expect(actual).to be(expected) или expect(actual).to equal(expected) тест проходит, если actual.equal?(expected) Оператор equal? в языке Ruby возвращает true только в том случае, если сравниваются две ссылки на один и то же объект: $ pry [1] pry(main)> 1.equal? 1 => true [2] pry(main)> "a".equal? "a" => false [3] pry(main)> s = "a" => "a" [4] pry(main)> s.equal? s => true «обычные» сравнения: o o o o o expect(actual).to be > expected expect(actual).to be >= expected expect(actual).to be <= expected expect(actual).to be < expected expect(actual).to be_within(delta).of(expected) Последнее из сравнений обычно используют вместо сравнения на эквивалентность (равенство) для действительных чисел (экземпляров класса Float), что связано с округлениями при операциях с ними: $ pry [1] pry(main)> (1.0/49.0)*49.0 == 1.0 => false Тесты для действительных чисел обычно выглядят примерно так: expect((1.0/49.0)*49.0).to be_within(1.0e-9).of(1.0) Это можно прочитать так: ожидается, что результат выполнения операции (1.0/49.0)*49.0 будет отличаться от 1.0 не более, чем на 1.0e-9. Иначе говоря, результат вычисления данного выражения будет находиться в -окрестности точки 1.0 для сравнения типов: o expect(actual).to be_an_instance_of(expected) этот тест проходит, если actual является экземпляром класса expected: $ pry [1] pry(main)> 1.instance_of?(Fixnum) => true [2] pry(main)> 1.instance_of?(Numeric) => false o expect(actual).to be_a_kind_of(expected) этот тест проходит, если actual является экземпляром класса expected или любого из его родительских классов: $ pry [1] pry(main)> 1.kind_of?(Fixnum) => true [2] pry(main)> 1.kind_of?(Numeric) => true [3] pry(main)> 1.kind_of?(Object) => true [4] pry(main)> 1.kind_of?(Float) => false проверки истинности: expect(actual).to be_true тест проходит, если actual отлично от nil и false o expect(actual).to be_false тест проходит, если actual равно nil или false o expect(actual).to be_nil тест проходит, если actual равно nil Полный перечень встроенных операций сравнения, используемых в RSpec, можно посмотреть, например, здесь. o 3.3. Содержательная сторона написания тестов Научиться правильно записывать (техническая сторона) утверждения о программе можно очень быстро. Сложнее понять, какие именно утверждения следует включать в тесты. Ведь если программист неправильно понимает задачу и пишет неправильный код программы, то что помешает ему написать и неправильные тесты (тесты, которые будут якобы говорить о правильности программы)? Какие утверждения нам следует написать для рассматриваемой задачи о мощности множества решений? Перебрать абсолютно все возможные варианты входных данных невозможно в силу бесконечности их количества. Если же пропустить хотя бы один, то может оказаться, что именно в этом случае программа и не работает. Из этого, кстати, следует, что чаще всего с помощью тестов нельзя доказать правильность программы. Так какие же утверждения включать в тесты? Желательно перебрать все существенно различные случаи входных данных, а чтобы это сделать, необходимо правильное понимание существа решаемой задачи. Наша программа, находящая мощность множества решений, умеет в качестве ответа выдать continuum, поэтому логично добавить соответствующее утверждение в тест: require './cardinality' describe do context 'Функция cardinality' do it 'возвращает 2 при a=1,b=1,c=0' do expect(cardinality(1,1,0)).to eq 2 end it 'возвращает continuum при a=0,b=0,c=0' do expect(cardinality(0,0,0)).to eq 'continuum' end end end Запустив тест, убеждаемся, что оба содержащихся в тесте утверждения истинны. Что бы нам ещё добавить в тест? В уже имеющихся утверждениях параметры принимали значения 0 и 1. Почему бы нам не попробовать перебрать все возможные наборы из нулей и единиц? Таких наборов, как известно, восемь ( require './cardinality' describe do context 'Функция cardinality' do it 'возвращает continuum при a=0,b=0,c=0' do expect(cardinality(0,0,0)).to eq 'continuum' end it 'возвращает ? при a=0,b=0,c=1' do expect(cardinality(0,0,1)).to eq ? end it 'возвращает ? при a=0,b=1,c=0' do expect(cardinality(0,1,0)).to eq ? end it 'возвращает ? при a=0,b=1,c=1' do expect(cardinality(0,1,1)).to eq ? ): end it 'возвращает ? при a=1,b=0,c=0' do expect(cardinality(1,0,0)).to eq ? end it 'возвращает ? при a=1,b=0,c=1' do expect(cardinality(1,0,1)).to eq ? end it 'возвращает 2 при a=1,b=1,c=0' do expect(cardinality(1,1,0)).to eq 2 end it 'возвращает ? при a=1,b=1,c=1' do expect(cardinality(1,1,1)).to eq ? end end end Во вновь добавленных утверждениях вместо ожидаемого результата работы функции cardinality мы пока писали знаки вопроса, которые теперь следует заменить на правильные значения. Каковы они? В результате мы получим тест со следующими утверждениями: require './cardinality' describe do context 'Функция cardinality' do it 'возвращает continuum при a=0,b=0,c=0' do expect(cardinality(0,0,0)).to eq 'continuum' end it 'возвращает 0 при a=0,b=0,c=1' do expect(cardinality(0,0,1)).to eq 0 end it 'возвращает 1 при a=0,b=1,c=0' do expect(cardinality(0,1,0)).to eq 1 end it 'возвращает 1 при a=0,b=1,c=1' do expect(cardinality(0,1,1)).to eq 1 end it 'возвращает 1 при a=1,b=0,c=0' do expect(cardinality(1,0,0)).to eq 1 end it 'возвращает 0 при a=1,b=0,c=1' do expect(cardinality(1,0,1)).to eq 0 end it 'возвращает 2 при a=1,b=1,c=0' do expect(cardinality(1,1,0)).to eq 2 end it 'возвращает 0 при a=1,b=1,c=1' do expect(cardinality(1,1,1)).to eq 0 end end end Давайте сразу же подумаем над важнейшим вопросом: все ли существенные варианты входных данных мы рассмотрели? Как это понять? Что представляет собой соотношение ? При ненулевых значениях это квадратное уравнение и мощность множества его решений зависит от знака дискриминанта. Если , a значение отлично от нуля, то уравнение становится линейным, поэтому у него всегда ровно одно решение. Если же , то всё зависит от : при имеется континуум решений, в противном случае их нет. Переставим уже написанные нами утверждения в порядке убывания степени уравнения и добавим комментарии: require './cardinality' describe do context 'Функция cardinality' do # Квадратное уравнение с положительным дискриминантом it 'возвращает 2 при a=1,b=1,c=0' do expect(cardinality(1,1,0)).to eq 2 end # Квадратное уравнение с нулевым дискриминантом it 'возвращает 1 при a=1,b=0,c=0' do expect(cardinality(1,0,0)).to eq 1 end # Квадратное уравнение с отрицательным дискриминантом it 'возвращает 0 при a=1,b=0,c=1' do expect(cardinality(1,0,1)).to eq 0 end it 'возвращает 0 при a=1,b=1,c=1' do expect(cardinality(1,1,1)).to eq 0 end # Линейное уравнение it 'возвращает 1 при a=0,b=1,c=0' do expect(cardinality(0,1,0)).to eq 1 end it 'возвращает 1 при a=0,b=1,c=1' do expect(cardinality(0,1,1)).to eq 1 end # Вырожденный случай 1 it 'возвращает continuum при a=0,b=0,c=0' do expect(cardinality(0,0,0)).to eq 'continuum' end # Вырожденный случай 2 it 'возвращает 0 при a=0,b=0,c=1' do expect(cardinality(0,0,1)).to eq 0 end end end Запуск даёт не вполне ожидаемый результат: $ rspec -c cardinality_spec.rb .......F Failures: 1) Функция cardinality возвращает 0 при a=0,b=0,c=1 Failure/Error: expect(cardinality(0,0,1)).to eq 0 expected: 0 got: 1 (compared using ==) # ./cardinality_spec.rb:37:in `block (3 levels) in ' Finished in 0.0011 seconds 8 examples, 1 failure Failed examples: rspec ./cardinality_spec.rb:36 # Функция cardinality возвращает 0 при a=0,b=0,c=1 Одно из утверждений оказалось ложным: наша программа выдаёт неверный результат при и . Внимательно посмотрим на код программы (файл cardinality.rb). Легко обнаружить следующее: мы ошибочно считали, что если уравнение не является квадратным, то мощность множества его решений равна либо бесконечности, либо единице. Мы «потеряли» тот самый случай, который встретился в тесте. Исправим ошибку: def cardinality(a,b,c) if a != 0 # Уравнение является квадратным d = b*b - 4*a*c if d < 0 0 elsif d > 0 2 else 1 end else # Уравнение не является квадратным if b != 0 # Уравнение является линейным 1 else if c == 0 "continuum" else 0 end end end end if $0 == __FILE__ print "Введите a -> " a = gets.to_i print "Введите b -> " b = gets.to_i print "Введите c -> " c = gets.to_i puts "Мощность множества решений уравнения равна #{cardinality(a,b,c)}." end