13. Тестване
23 ноември 2015
Днес
- Тестове и тестване
- Unit, интеграционни
- Тестове на уеб, GUI, CLI, API-та
- RSpec примери
Шеста задача
- Може би си мислите, че ни е омръзнало
- Не! Още днес ще дадем шеста задача
- Това е най-добрият начин да научите един език
Софтуерът и електрониката
Да сравним писането на софтуер с електрониката
Нещата, които правим, обикновено изглеждат така:
Мотивация
- Вълнуващо е, когато "творението" проработи
- Чувството да си създател на нещо е окрилящо и мотивиращо
- Чувството за завършен краен продукт - също
Хвърчащият монтаж - плюсове
- Почти не изисква предварително планиране
- Пести време
- Подходящ за експерименти и изследване на непознати територии
- Бързото достигане до работещ продукт носи удовлетворение
Хвърчащият монтаж - проблеми
- Изработеното изделие е много крехко и чупливо
- Много трудно позволява модификации от един момент нататък
- Ако има проблем, е много трудно да се открие и да се отстрани
- Ако го оставите за месец, ще забравите всичко за него
- Няма "пазарен" и "завършен" вид
Продукт
Хардуерът в "хвърчащ монтаж" не е завършен продукт.
Legacy код
кошмарът на всеки програмист
Какво е legacy код?
- "Legacy code is source code that relates to a no-longer supported or manufactured operating system or other computer technology."
- Стар код, който трябва да се поддържа
- Legacy код е всеки код без автоматизирани тестове
Код без тестове = legacy код
- Всеки ред код без тестове е равносилен на хвърчащ монтаж
- Носи същите плюсове и минуси
- Всеки дълготраен проект и продукт трябва да има тестове
- Само така ще бъде завършен продукт
Лош опит
Аз съм допринесъл много за света с код тип "хвърчащ монтаж".
Достигнах до някои изводи по трудния начин.
Завършен продукт = функционалност + тестове
Едва напоследък започнах да виждам софтуера като завършен продукт, с кутия и пазарен вид, когато идва с пълен пакет автоматизирани тестове.
Няма лесен път към просветлението
- Да пишете тестове не е лесно
- В началото ще ви отнема до два пъти повече време
- Има много мотики, които трябва да и ще настъпите
- "The One True Way®" не съществува
- Трябва да извървите голяма част от пътя сами
- Има с какво да си помагате
To test or not to test?
Едно е сигурно - без тестове не може.
Затова затягайте коланите и поемайте по пътя.
In testing I trust!
- Не всеки ред код трябва да се тества
- Не винаги трябва да тествате даден проект (например - прототипизиране)
- Различни техники на тестване ви носят различни плюсове и минуси - преценявайте според ситуацията
- Както всяко правило и добра практика, не следвайте чужди съвети на сляпо
- Пробвайте какво работи за вас, за екипа ви, за проекта
- Опитайте да научите колкото можете от чужди грешки и сполуки, но...
- Само опитът ви ще ви помогне
Терминология
- Понякога има вариации в термините
- Ще опитаме да покрием основните, валидни за повечето среди и езици за програмиране
Unit тестове
- Тестване на една сравнително атомарна "единица" софтуер ("unit")
- На практика, обикновено това са (публичните) методи на даден клас
- Така тествате класовете си и методите си
- Така тестваме решенията на домашните ви
Интеграционни тестове
- Тестват няколко компонента (или цялата система) в интеграция
- Например, тест за логин на потребител - тества цялото уеб приложение, включително базата данни
- Близки термини: "acceptance" тестове, "end-to-end" тестове
- Обикновено са "black-box" тестове - приемат, че по-малките компоненти са unit-тествани
- Много полезни, но бавни
Assertion (твърдение, проверка)
An assertion is a function or macro that verifies the behavior (or the state) of the unit under test. Usually an assertion expresses a logical condition that is true for results expected in a correctly running system under test (SUT). Failure of an assertion typically throws an exception, aborting the execution of the current test.
- С други думи, единича проверка на даден факт
- Например, ако събера 2 + 2, очаквам да получа 4
assert(2 + 2 == 4, 'Wait, what?')
Test Fixtures
A test fixture (also known as a test context) is the set of preconditions or state needed to run a test. The developer should set up a known good state before the tests, and return to the original state after the tests.
- Това не означава само записи в база данни
- Може да са файлове с определени данни - картинки, имейли и прочее
- За всеки език има техники и библиотеки, които помагат с връщането на състоянието на базата
Test case
- В случая на софтуер - една тематична проверка на поведението на софтуера в конкретна ситуация
- В един test case може да има една или няколко проверки
- Обикновено се препоръчва да не са повече от една
- Например -
test_can_withdraw_when_enough_amount_available
- Или -
test_cannot_withdraw_when_not_enough_amount_available
- Обикновено, освен проверки, test case-ът съдържа и някаква форма на подготовка (setup) на средата
setup/teardown, before/after
setup
е код, който се изпълнява преди всеки тест (test case) и подготвя "света" и състоянието за теста (fixtures)
teardown
е същото, но след теста има за задача да върне нещата както са били и да направи cleanup
- В различните test frameworks имат различни имена, но смислово правят това
- Обикновено има възможност и за групиране на общ setup код за една тематична група от тестове
Test Suite
A test suite is a set of tests that all share the same fixture. The order of the tests shouldn't matter.
- Горното е дефиницията на този термин в xUnit / SUnit
- Понякога "test suite" се нарича цялата съвкупност от тестове на даден проект
Test Runner
- Просто софтуер, който изпълнява тестовете ви
- Има различни варианти за това
- Често се налага да може да пуснете само един конкретен тест case, или само unit-тестовете на даден клас
- Различни режими на работа и различно форматиране и оцветяване на изхода (резултатите)
Общи принципи
- Не тествате private методи
- Ако private методите ви се струват сложни и че е нужно тестване, значи е нужно да ги изведете в отделен клас
- Не използвате random данни в тестовете - необходимо ви е предвидимо поведение; ако веднъж стане failure, трябва да може да го пресъздадете
- Тествате в изолация и предвидимо обкръжение - правите setup
- Горното включва дори time traveling в някои ситуации (gem: timecop) - немалко хора са имали failing test заради DST промяна, или сменена часова зона
Скорост
- Трябва да може да проверите за части от секундата дали unit тестовете на класа, който пишете, минават или не
- Затова тестовете трябва да са бързи - за да дават максимално бързо feedback
- Обикновено не пускате всички тестове на всяка промяна, но често - няколко пъти в минута - пускате unit-тестовете на дадения клас/модул/код
- Затова и тестовете трябва да могат да работят в максимална изолация
TDD
test-driven development
- Методология (философия?) за разработване на софтуер
- Някои хора твърдят, че за тях води до по-добра архитектура, по-ясен и прост код
- Red → Green → Refactor
- Red - пишете тест без код и пускате, за да се уверите, че не е такъв, който винаги минава
- Green - пишете минималния код, колкото да мине теста (stub-вате), за да проверите, че тестът не е такъв, който винаги fail-ва
- Refactor - знаете, че имате работещ тест; рефакторирате кода на спокойствие и си пускате теста след всяка промяна
- Разчита на изолация, скорост и фокус на тестовете
- Test Driven Development By Example
BDD
behaviour-driven development
- Методология като TDD
- Разликата е в начина, по който се описват тестовете
проверка дали A прави Б
<> A прави Б
провери дали А е равно на Б
<> очаквам, че А е равно на Б
- Описание на проверка <> описание на поведение
- Императивна дефиниция <> декларативна дефиниция
Test::Unit
<> RSpec
Continuous Integration (CI)
- Машина/процес, която автоматично пуска тестовете, когато някой push-не в определен branch
- Веднага сигнализира, ако някой commit-не код, който чупи тестовете
- Понякога тестовете (или поне някои от тях) са по-бавни; CI-ят се грижи да ги пуска когато трябва, асинхронно от разработката на програмиста
- Популярна услуга за това за OSS проекти – Travis CI
Метрики
- Code coverage - колко процента от редовете код са покрити от тестове
- Обикновено се мери кои редове код се изпълняват и кои не, докато работят тестовете
- Това е косвена метрика за покритие и още по-косвена - за качество на кода
Тестване в Ruby
- Няколко различни варианта
- Test::Unit
- Minitest
- RSpec
- Capybara, Cucumber
Test::Unit - assertions
assert(boolean, message = nil)
assert_equal(expected, actual, message = nil)
assert_in_delta(expected_float, actual_float, delta, message = "")
assert_match(pattern, string, message = "")
assert_nil(object, message = "")
assert_not_nil(object, message = "")
assert_raises(*args, &block)
- Списък с наличните assertions
Test::Unit - пример
require 'test/unit'
class TC_MyTest < Test::Unit::TestCase
# def setup
# end
# def teardown
# end
def test_fail
assert(false, 'Assertion was false.')
end
end
Minitest
- "minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking."
- Seattle.rb проект
- Част от стандартната библиотека от Ruby 1.9 насам
- Съставен от Minitest::Unit, Minitest::Spec, Minitest::Mock и Minitest::Benchmark
- Документация
Генериране на тестови данни
- Fixtures - предварително подготвени тестови данни, често за голям брой тестове
- Factories (FactoryGirl) - създават данни само за определени тестове. По-гранулярни
Тестване на GUI
- Unit-тествате кода отзад и не държите бизнес логика в GUI-specific код
- Пишете интеграционни тестове за GUI
- Често през accessibility функционалност
Тестване на CLI
- Същото като за GUI
- Обикновено освен отделянето на бизнес кода, и самият bin файл е капсулиран в отделен клас - option handling-а и прочее - и може да се unit-тества
- Интеграционният тест може и да вика изпълнимия файл, макар че това ще е по-бавно
Тестване на API-клиенти
- Моквате API-то - правите му симулация и тествате спрямо нея
- Избягвате тестове спрямо live API-то - непредвидими и бавни (networking)
- Полезен gem за целта - vcr
- "Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests."
Тестване на уеб
- Rack::Test
- Capybara
- Cucumber
Rack::Test
Rack::Test is a small, simple testing API for Rack apps. It can be used on its own or as a reusable starting point for Web frameworks and testing libraries to build on.
- "Maintains a cookie jar across requests"
- "Easily follow redirects when desired"
- "Set request headers to be used by all subsequent requests"
- "Small footprint. Approximately 200 LOC"
- github.com/brynary/rack-test
Rack::Test - пример
require 'rack/test'
describe 'Homepage' do
include Rack::Test::Methods
it 'says hello' do
authorize 'brian', 'secret'
get '/'
expect(last_response).to be_ok
expect(last_response.body).to eq 'Hello, Brian!'
end
end
Capybara
https://github.com/jnicklas/capybara
- Интеграционни тестове
- По подразбиране ползва Rack::Test и симулира HTTP заявки
- Проверявате дали резултата отговаря на очакванията ви
- Горното е бързо, но не поддържа JS и реални HTTP заявки към външни услуги
- Затова - Capybara-WebKit, Selenium (webdriver) или Poltergeist (интеграция с PhantomJS)
Capybara - пример
describe 'the signin process' do
before(:each) do
User.create(email: 'user@example.com', password: 'password')
end
it 'signs me in' do
visit '/sessions/new'
within '#session' do
fill_in 'Email', with: 'user@example.com'
fill_in 'Password', with: 'password'
end
click_button 'Sign in'
expect(page).to have_content 'Success'
end
end
Въпроси дотук?
Имате ли въпроси по нещата досега?
Следват няколко примера с RSpec
Тестване на методи
class User
# Can be one of `:user`, `:admin`
attr_accessor :rank
def initialize(name, rank)
@name = name
@rank = rank
end
def admin?
rank == :admin
end
end
Тестване на методи
describe User do
describe '#admin?' do
it 'is true for admins' do
expect(User.new('John', :admin).admin?).to be true
end
it 'is false for non-admins' do
expect(User.new('John', :user).admin?).to be false
end
end
end
context
describe User do
describe '#admin?' do
context 'when the user is an admin' do
it 'is true' do
expect(User.new('John', :admin).admin?).to be true
end
end
context 'with a regular user' do
it 'is false' do
expect(User.new('John', :user).admin?).to be false
end
end
end
end
Друг пример
class Game
def initialize(name, genre)
@name = name
@genre = genre
end
def recommend
case genre
when :mmorpg then "Hey! Did you hear about #{name}? It's better than WoW!"
when :fps then "Yo! You must try this new shooter - #{name}!"
else "Have you tried #{name}? It's awesome!"
end
end
end
describe Game do
describe '#recommend' do
context 'when the game is an MMORPG' do
it 'compares it to WoW' do
game = Game.new('Guild Wars 2', :mmorpg)
expect(game.recommend).to eq 'Hey! Did you hear about Guild Wars 2? It\'s better than WoW!'
end
end
...
...
context 'when the game is an FPS' do
it 'calls it a shooter' do
game = Game.new('FarCry 4', :fps)
expect(game.recommend).to eq 'Yo! You must try this new shooter - FarCry 4!'
end
end
...
...
context 'when the game is of an unknown genre' do
it 'says it\'s awesome' do
game = Game.new('The Witcher 3', :rpg)
expect(game.recommend).to eq 'Have you tried The Witcher 3? It\'s awesome!'
end
end
To mock or not to mock
class User
def like(game)
game.likes += 1
end
end
Без mock-ване
it 'increases the like counter of the game' do
game = Game.new('The Witcher 3', :rpg)
user = User.new('Georgi', :admin)
user.like(game)
expect(game.likes).to eq 1
end
С mock-ване
it 'increases the like counter of the game' do
game = double
user = User.new('Georgi', :admin)
expect(game).to receive(:likes=).with(1)
user.like(game)
end
Двете страни на една и съща монета
ЗА мокване
- Тестваме поведението на метода
- Ако се счупи
Game
, няма да се счупи теста
- Избягваме подаване на ненужни тестови данни
Двете страни на една и съща монета
ПРОТИВ мокване
- Тестваме състоянието на резултата
- Ако променим поведението на
Game
, може да пропуснем да променим и User
- В някои случаи ни трябват допълнителни методи за проверка на състоянието
Често използвани обекти
let
let(:user) { User.new('Georgi', :admin) }
let(:game) { Game.new('The Witcher 3', :rpg) }
it 'can like a game' do
user.like(game)
expect(game.likes).to eq 1
expect(user.favourite_games).to match_array [game]
end
it 'can play a game' do
user.play(game, 2.hours)
expect(game.played_hours).to eq 2
expect(user.gameplay_hours).to eq 2
end
Stub-ване
class Game
def popular?() likes >= 100 end
end
let(:game) { Game.new('The Witcher 3', :rpg) }
describe '#popular?' do
it 'returns true if the game is liked by at least 100 users' do
allow(game).to receive(:likes).and_return(123)
expect(game.popular?).to be true
end
end
Друг пример
class NukeSilo
def launch_nukes_at(city)
# [CLASSIFIED]
"Nukes sent at #{city}"
end
end
class User
def launch_nukes(silo)
raise "No permissions" unless rank == :superadmin
silo.launch_nukes_at '[CLASSIFIED]'
end
end
Друг пример
context 'when the user is a superadmin' do
let(:silo) { NukeSilo.new }
it 'launches the nukes and reports what happened' do
user = User.new('Obama', :superadmin)
expect(silo).to receive(:launch_nukes_at).and_return('World peace achieved')
expect(user.launch_nukes(silo)).to eq 'World peace achieved'
end
end
Before
describe '#launch_nukes' do
before do
@silo = NukeSilo.new
allow(@silo).to receive(:launch_nukes_at).and_return('World peace achieved')
end
# ...
end
before
, before(:each)
- изпълнява се преди всеки тест от групата
before(:all)
- изпълнява се веднъж преди всички тестове от групата
- Инстанционни променливи, дефинирани в блока, могат да се достъпват от тестовете (примерите)
RSpec - повече информация