22. Тестване
12 януари 2015
Днес
- Тестове и тестване
- Unit, интеграционни
- Тестове на уеб, GUI, CLI, API-та
Пета задача
- Последната, пета задача е публикувана
- Задачата е ретроспективна и с малко по-различни правила от досегашните домашни
- Добър шанс да решите пропуснати задачи и да научите пропуснати неща
- Трябва да клонирате това хранилище: github.com/fmi/ruby-retrospective-4
- Имате време до следващия понеделник
Кратка история
Преди години се занимавах с електроника като хоби.
Нещата, които правех, обикновено изглеждаха така:
Мотивация
- Вълнуващо е, когато "творението" проработи
- Чувството да си създател на нещо е окрилящо и мотивиращо
- Чувството за завършен краен продукт - също
Хвърчащият монтаж - плюсове
- Почти не изисква предварително планиране
- Пести време
- Подходящ за експерименти и изследване на непознати територии
- Бързото достигане до работещ продукт носи удовлетворение
Хвърчащият монтаж - проблеми
- Изработеното изделие е много крехко и чупливо
- Много трудно позволява модификации от един момент нататък
- Ако има проблем, е много трудно да се открие и да се отстрани
- Ако го оставите за месец, ще забравите всичко за него
- Няма "пазарен" и "завършен" вид
Продукт
Хардуерът в "хвърчащ монтаж" не е завършен продукт.
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-тествани
- Много полезни, но бавни
xUnit
xUnit is the collective name for several unit testing frameworks that derive their structure and functionality from Smalltalk's SUnit. SUnit, designed by Kent Beck in 1998, was written in a highly-structured object-oriented style.
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
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 - знаете, че имате работещ тест; рефакторирате кода на спокойствие и си пускате теста след всяка промяна
- Разчита на изолация, скорост и фокус на тестовете
Continuous Integration
- Машина/процес, която автоматично пуска тестовете, когато някой push-не в определен branch
- Веднага сигнализира, ако някой commit-не код, който чупи тестовете
- Понякога тестовете (или поне някои от тях) са по-бавни; CI-ят се грижи да ги пуска когато трябва, асинхронно от разработката на програмиста
- Популярна услуга за това за OSS проекти – Travis CI
Метрики
- Code coverage - колко процента от редовете код са покрити от тестове
- Обикновено се мери кои редове код се изпълняват и кои не, докато работят тестовете
- Това е косвена метрика за покритие и още по-косвена - за качество на кода
- Също - code LOC спрямо test LOC
Тестване в Ruby
- Няколко различни варианти
- Test::Unit
- Minitest
- RSpec
- Capybara, Cucumber
- SimpleCov
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
RSpec - пример
RSpec.describe Account do
it "has a balance of zero when first created" do
expect(Account.new.balance).to eq(Money.new(0))
end
end
RSpec - повече информация
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
- Документация
Компоненти на Minitest
- "minitest/unit is a small and incredibly fast unit testing framework. It provides a rich set of assertions to make your tests clean and readable."
- "minitest/spec is a functionally complete spec engine. It hooks onto minitest/unit and seamlessly bridges test assertions over to spec expectations."
- "minitest/benchmark is an awesome way to assert the performance of your algorithms in a repeatable manner. Now you can assert that your newb co-worker doesn't replace your linear algorithm with an exponential one!"
- "minitest/mock is a beautifully tiny mock (and stub) object framework."
Minitest::Unit
class TestMeme < Minitest::Test
def setup
@meme = Meme.new
end
def test_that_kitty_can_eat
assert_equal "OHAI!", @meme.i_can_has_cheezburger?
end
def test_that_it_will_not_blend
refute_match /^no/i, @meme.will_it_blend?
end
def test_that_will_be_skipped
skip "test this later"
end
end
Minitest::Spec
describe Meme do
before do
@meme = Meme.new
end
describe "when asked about cheeseburgers" do
it "must respond positively" do
@meme.i_can_has_cheezburger?.must_equal "OHAI!"
end
end
describe "when asked about blending possibilities" do
it "won't say no" do
@meme.will_it_blend?.wont_match /^no/i
end
end
end
Генериране на тестови данни
- Fixtures в Rails
- FactoryGirl
- Faker
- Пак - избягвайте random; използвайте series и подобни
Тестване на 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"
class HomepageTest < Test::Unit::TestCase
include Rack::Test::Methods
def test_redirect_logged_in_users_to_dashboard
authorize "bryan", "secret"
get "/"
follow_redirect!
assert_equal "http://example.org/redirected", last_request.url
assert last_response.ok?
end
end
Capybara
https://github.com/jnicklas/capybara
- Интеграционни тестове
- Ползва Rack::Test и симулира HTTP заявки
- Проверявате дали резултата отговаря на очакванията ви
- Горното е бързо, но не поддържа JS и реални HTTP заявки към външни услуги
- Затова - Selenium (webdriver), Capybara-webkit или Poltergeist (интеграция с PhantomJS)
Capybara - пример
describe "the signin process" do
before :each do
User.make(: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
PhantomJS
phantomjs.org
PhantomJS is a headless WebKit scriptable with a JavaScript API. It has fast and native support for various web standards: DOM handling, CSS selector, JSON, Canvas, and SVG.
- Много широка гама от възможни употреби
- Най-простата - да направите screenshot на страница и да го запазите в PNG
PhantomJS - пример
просто нещо готино
console.log('Loading a web page');
var page = require('webpage').create();
var url = 'http://www.phantomjs.org/';
page.open(url, function (status) {
//Page is loaded!
phantom.exit();
});
Cucumber
Making BDD fun
- Behaviour driven development
- cukes.info и github.com/cucumber/cucumber
- "Cucumber is a tool that executes plain-text functional descriptions as automated tests. The language that Cucumber understands is called Gherkin."
- Може да започнете с Wiki-то
- Има и доста книги по темата
Gherkin
Feature: Search courses
Courses should be searchable by topic
Search results should provide the course code
Scenario: Search by topic
Given there are 240 courses which do not have the topic "biology"
And there are 2 courses, A001 and B205, that each have "biology" as one of the topics
When I search for "biology"
Then I should see the following courses:
| Course code |
| A001 |
| B205 |
Пример с Gherkin и Cucumber